feat(project-crud-sync): add planning artifacts for CRUD project lifecycle #5
|
|
@ -71,6 +71,7 @@
|
|||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^@workspace/schema$": "<rootDir>/../test/__mocks__/schema.ts",
|
||||
"^@workspace/schema/migrations$": "<rootDir>/../test/__mocks__/schema-migrations.ts",
|
||||
"^valibot$": "<rootDir>/../test/__mocks__/valibot.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
||||
import { Pool } from "pg";
|
||||
import { migrationsDir } from "@workspace/schema/migrations";
|
||||
import {
|
||||
drizzle,
|
||||
NodePgDatabase,
|
||||
|
|
@ -8,7 +9,6 @@ import {
|
|||
tasks,
|
||||
projectsRelations,
|
||||
tasksRelations,
|
||||
migrationsDir,
|
||||
} from "@workspace/schema";
|
||||
|
||||
const DEFAULT_PG_PORT = 5432;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const DEFAULT_PORT = 3001;
|
|||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors();
|
||||
await app.listen(process.env.PORT ?? DEFAULT_PORT);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
|
|
|
|||
1
apps/api/test/__mocks__/schema-migrations.ts
Normal file
1
apps/api/test/__mocks__/schema-migrations.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const migrationsDir = "/mock/migrations";
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
},
|
||||
"moduleNameMapper": {
|
||||
"^@workspace/schema$": "<rootDir>/__mocks__/schema.ts",
|
||||
"^@workspace/schema/migrations$": "<rootDir>/__mocks__/schema-migrations.ts",
|
||||
"^valibot$": "<rootDir>/__mocks__/valibot.ts"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
|
|
|
|||
|
|
@ -4,5 +4,8 @@ export default defineAppConfig({
|
|||
primary: "green",
|
||||
neutral: "slate",
|
||||
},
|
||||
main: {
|
||||
base: "min-h-0 flex-1 overflow-y-auto",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,77 +1,64 @@
|
|||
<script setup>
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: "viewport", content: "width=device-width, initial-scale=1", },
|
||||
],
|
||||
link: [
|
||||
{ rel: "icon", href: "/favicon.ico", },
|
||||
],
|
||||
meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
|
||||
link: [{ rel: "icon", href: "/favicon.ico" }],
|
||||
htmlAttrs: {
|
||||
lang: "en",
|
||||
},
|
||||
},)
|
||||
});
|
||||
|
||||
const title = "Nuxt Starter Template"
|
||||
const description = "A production-ready starter template powered by Nuxt UI. Build beautiful, accessible, and performant applications in minutes, not hours."
|
||||
const title = "Work Hub";
|
||||
const description = "Project management made simple.";
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
ogImage: "https://ui.nuxt.com/assets/templates/nuxt/starter-light.png",
|
||||
twitterCard: "summary_large_image",
|
||||
},)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #left>
|
||||
<NuxtLink to="/">
|
||||
<AppLogo class="w-auto h-6 shrink-0" />
|
||||
</NuxtLink>
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<UHeader>
|
||||
<template #left>
|
||||
<NuxtLink to="/">
|
||||
<AppLogo class="w-auto h-6 shrink-0" />
|
||||
</NuxtLink>
|
||||
|
||||
<TemplateMenu />
|
||||
</template>
|
||||
<UButton to="/projects" label="Projects" color="neutral" variant="ghost" />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UButton
|
||||
to="https://github.com/nuxt-ui-templates/starter"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
/>
|
||||
</template>
|
||||
</UHeader>
|
||||
<UMain class="px-6">
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UMain>
|
||||
|
||||
<UMain>
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
<USeparator icon="i-simple-icons-nuxtdotjs" />
|
||||
|
||||
<USeparator icon="i-simple-icons-nuxtdotjs" />
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p class="text-sm text-muted">Built with Nuxt UI • © {{ new Date().getFullYear() }}</p>
|
||||
</template>
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p class="text-sm text-muted">
|
||||
Built with Nuxt UI • © {{ new Date().getFullYear() }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
to="https://github.com/nuxt-ui-templates/starter"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
/>
|
||||
</template>
|
||||
</UFooter>
|
||||
<template #right>
|
||||
<UButton
|
||||
to="https://github.com/nuxt-ui-templates/starter"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
color="neutral"
|
||||
variant="ghost" />
|
||||
</template>
|
||||
</UFooter>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
|
|
|||
54
apps/web/app/components/ProjectForm.vue
Normal file
54
apps/web/app/components/ProjectForm.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<script setup lang="ts">
|
||||
import { PROJECT_STATUSES, type ProjectStatus } from "@workspace/schema/constants";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
status?: ProjectStatus;
|
||||
saving?: boolean;
|
||||
}>(),
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
status: "active",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [body: { name: string; description?: string; status: string }];
|
||||
}>();
|
||||
|
||||
const name = ref(props.name);
|
||||
const description = ref(props.description);
|
||||
const status = ref(props.status);
|
||||
|
||||
function onSubmit() {
|
||||
emit("submit", {
|
||||
name: name.value,
|
||||
description: description.value || undefined,
|
||||
status: status.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="space-y-6" @submit.prevent="onSubmit">
|
||||
<UFormField label="Name" required>
|
||||
<UInput v-model="name" placeholder="Project name" class="w-full" required />
|
||||
|
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Description">
|
||||
<UTextarea v-model="description" placeholder="Optional description" class="w-full" :rows="3" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Status">
|
||||
<USelect v-model="status" :items="PROJECT_STATUSES.map((s) => ({ label: s, value: s }))" class="w-full" />
|
||||
</UFormField>
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" to="/projects" />
|
||||
<UButton type="submit" label="Save" color="primary" :loading="saving" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
@ -2,48 +2,55 @@
|
|||
<UDropdownMenu
|
||||
v-slot="{ open }"
|
||||
:modal="false"
|
||||
:items="[{
|
||||
label: 'Starter',
|
||||
to: 'https://starter-template.nuxt.dev/',
|
||||
color: 'primary',
|
||||
checked: true,
|
||||
type: 'checkbox',
|
||||
}, {
|
||||
label: 'Landing',
|
||||
to: 'https://landing-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'Docs',
|
||||
to: 'https://docs-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'SaaS',
|
||||
to: 'https://saas-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'Dashboard',
|
||||
to: 'https://dashboard-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'Chat',
|
||||
to: 'https://chat-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'Portfolio',
|
||||
to: 'https://portfolio-template.nuxt.dev/',
|
||||
}, {
|
||||
label: 'Changelog',
|
||||
to: 'https://changelog-template.nuxt.dev/',
|
||||
}]"
|
||||
:items="[
|
||||
{
|
||||
label: 'Starter',
|
||||
to: 'https://starter-template.nuxt.dev/',
|
||||
color: 'primary',
|
||||
checked: true,
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Landing',
|
||||
to: 'https://landing-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
to: 'https://docs-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'SaaS',
|
||||
to: 'https://saas-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
to: 'https://dashboard-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'Chat',
|
||||
to: 'https://chat-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'Portfolio',
|
||||
to: 'https://portfolio-template.nuxt.dev/',
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
to: 'https://changelog-template.nuxt.dev/',
|
||||
},
|
||||
]"
|
||||
:content="{ align: 'start' }"
|
||||
:ui="{ content: 'min-w-fit' }"
|
||||
size="xs"
|
||||
>
|
||||
size="xs">
|
||||
<UButton
|
||||
label="Starter"
|
||||
variant="subtle"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
size="xs"
|
||||
class="-mb-[6px] font-semibold rounded-full truncate"
|
||||
class="-mb-1.5 font-semibold rounded-full truncate"
|
||||
:class="[open && 'bg-primary/15']"
|
||||
:ui="{
|
||||
trailingIcon: ['transition-transform duration-200', open ? 'rotate-180' : undefined].filter(Boolean).join(' '),
|
||||
}"
|
||||
/>
|
||||
}" />
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Project } from "@workspace/schema";
|
||||
import type { Project, ProjectInsertDTO, ProjectUpdateDTO } from "@workspace/schema";
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T | null;
|
||||
|
|
@ -47,7 +47,7 @@ export function useProjectApi() {
|
|||
loading.value = false;
|
||||
}
|
||||
|
||||
async function createProject(body: Record<string, unknown>): Promise<Project | null> {
|
||||
async function createProject(body: Partial<ProjectInsertDTO>): Promise<Project | null> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export function useProjectApi() {
|
|||
return data;
|
||||
}
|
||||
|
||||
async function updateProject(id: string, body: Record<string, unknown>): Promise<Project | null> {
|
||||
async function updateProject(id: string, body: Partial<ProjectUpdateDTO>): Promise<Project | null> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ export function useProjectApi() {
|
|||
});
|
||||
|
||||
if (data) {
|
||||
const idx = projects.value.findIndex((p) => p.id === id);
|
||||
const idx = projects.value.findIndex(p => p.id === id);
|
||||
if (idx !== NOT_FOUND) projects.value[idx] = data;
|
||||
} else {
|
||||
error.value = err;
|
||||
|
|
@ -95,7 +95,7 @@ export function useProjectApi() {
|
|||
if (err) {
|
||||
error.value = err;
|
||||
} else {
|
||||
projects.value = projects.value.filter((p) => p.id !== id);
|
||||
projects.value = projects.value.filter(p => p.id !== id);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
|
|
|
|||
3
apps/web/app/layouts/default.vue
Normal file
3
apps/web/app/layouts/default.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
62
apps/web/app/pages/projects/[id]/edit.vue
Normal file
62
apps/web/app/pages/projects/[id]/edit.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import type { Project } from "@workspace/schema";
|
||||
|
||||
definePageMeta({
|
||||
title: "Edit Project",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { updateProject } = useProjectApi();
|
||||
|
||||
const saving = ref(false);
|
||||
const project = ref<Project | null>(null);
|
||||
const loading = ref(true);
|
||||
const notFound = ref(false);
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const apiBase = config.public.apiBaseUrl as string;
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await fetch(`${apiBase}/projects/${route.params.id}`);
|
||||
if (!res.ok) {
|
||||
notFound.value = true;
|
||||
} else {
|
||||
project.value = await res.json();
|
||||
}
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
async function handleSubmit(body: { name: string; description?: string; status: string }) {
|
||||
saving.value = true;
|
||||
const updated = await updateProject(route.params.id as string, body);
|
||||
saving.value = false;
|
||||
if (updated) router.push("/projects");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader title="Edit Project" description="Update project details">
|
||||
|
NHoarau
commented
Not working Not working
|
||||
<template #right>
|
||||
<UButton label="Back" icon="i-lucide-chevron-left" color="neutral" variant="outline" to="/projects" />
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<UPageBody>
|
||||
<UAlert v-if="notFound" color="warning" variant="soft" icon="i-lucide-alert-triangle" title="Project not found" />
|
||||
|
||||
<UProgress v-else-if="loading" />
|
||||
|
||||
<UCard v-else-if="project">
|
||||
|
NHoarau
commented
Explore suspense for this async case Explore suspense for this async case
|
||||
<ProjectForm
|
||||
:name="project.name"
|
||||
:description="project.description ?? undefined"
|
||||
:status="project.status"
|
||||
:saving
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</UCard>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
117
apps/web/app/pages/projects/index.vue
Normal file
117
apps/web/app/pages/projects/index.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import type { TableColumn } from "@nuxt/ui";
|
||||
import type { Project } from "@workspace/schema";
|
||||
|
||||
definePageMeta({
|
||||
title: "Projects",
|
||||
});
|
||||
|
||||
const { projects, loading, error, fetchProjects, deleteProject } = useProjectApi();
|
||||
|
||||
const deleting = ref<string | null>(null);
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
onMounted(() => fetchProjects());
|
||||
|
||||
function statusColor(status: string) {
|
||||
if (status === "active") return "success" as const;
|
||||
if (status === "archived") return "neutral" as const;
|
||||
if (status === "completed") return "info" as const;
|
||||
return "neutral" as const;
|
||||
}
|
||||
|
||||
async function confirmAndDelete(id: string) {
|
||||
deleting.value = id;
|
||||
await deleteProject(id);
|
||||
deleting.value = null;
|
||||
confirmDelete.value = false;
|
||||
}
|
||||
|
||||
const columns: TableColumn<Project>[] = [
|
||||
{ accessorKey: "name", header: "Name" },
|
||||
{ accessorKey: "status", header: "Status" },
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description",
|
||||
cell: ({ row }) => row.getValue("description") ?? "—",
|
||||
},
|
||||
{ accessorKey: "createdAt", header: "Created" },
|
||||
{ id: "actions", header: "" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader
|
||||
title="Projects"
|
||||
description="Manage your projects"
|
||||
:links="[{ label: 'New project', icon: 'i-lucide-plus', to: '/projects/new', color: 'primary' }]" />
|
||||
|
||||
<UPageBody>
|
||||
<UAlert v-if="error" color="error" variant="soft" icon="i-lucide-alert-circle" :title="error" class="mb-4" />
|
||||
|
||||
<UCard>
|
||||
<UTable :columns="columns" :data="projects" :loading="loading" class="flex-1">
|
||||
<template #status-cell="{ row }">
|
||||
<UBadge
|
||||
:label="row.getValue('status')"
|
||||
:color="statusColor(row.getValue('status') as string)"
|
||||
variant="subtle"
|
||||
class="capitalize" />
|
||||
</template>
|
||||
|
||||
<template #createdAt-cell="{ row }">
|
||||
<span class="text-muted">{{ new Date(row.getValue("createdAt") as string).toLocaleDateString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #actions-cell="{ row }">
|
||||
<div class="flex justify-end gap-1">
|
||||
<UButton
|
||||
icon="i-lucide-pencil"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:to="`/projects/${row.original.id}/edit`"
|
||||
aria-label="Edit" />
|
||||
<UButton
|
||||
icon="i-lucide-trash-2"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:loading="deleting === row.original.id"
|
||||
aria-label="Delete"
|
||||
@click="
|
||||
confirmDelete = true;
|
||||
deleting = row.original.id;
|
||||
" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="py-12 text-center text-muted">
|
||||
<UIcon name="i-lucide-folder-open" class="size-8 mx-auto mb-2" />
|
||||
<p>No projects yet.</p>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
<UModal v-model:open="confirmDelete">
|
||||
<template #header>
|
||||
<h3 class="text-lg font-semibold">Delete project</h3>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<p class="text-sm text-muted">Are you sure you want to delete this project? This action cannot be undone.</p>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="confirmDelete = false" />
|
||||
<UButton label="Delete" color="error" @click="deleting && confirmAndDelete(deleting)" />
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
34
apps/web/app/pages/projects/new.vue
Normal file
34
apps/web/app/pages/projects/new.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: "New Project",
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { createProject } = useProjectApi();
|
||||
const saving = ref(false);
|
||||
|
||||
async function handleSubmit(body: { name: string; description?: string; status: string }) {
|
||||
saving.value = true;
|
||||
const project = await createProject(body);
|
||||
saving.value = false;
|
||||
if (project) router.push("/projects");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader
|
||||
title="New Project"
|
||||
description="Create a new project"
|
||||
/>
|
||||
|
||||
<UPageBody>
|
||||
<UCard>
|
||||
<ProjectForm
|
||||
:saving
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</UCard>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
|
|
@ -6,5 +6,9 @@ export default withNuxt({
|
|||
rules: {
|
||||
...baseRules,
|
||||
"@stylistic/member-delimiter-style": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/html-closing-bracket-newline": "off",
|
||||
"vue/no-multiple-template-root": "off",
|
||||
},
|
||||
},)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./constants": {
|
||||
"types": "./src/constants.ts",
|
||||
"import": "./src/constants.ts",
|
||||
"default": "./src/constants.ts"
|
||||
},
|
||||
"./migrations": {
|
||||
"types": "./dist/migrations.d.ts",
|
||||
"import": "./dist/migrations.js",
|
||||
"default": "./dist/migrations.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { defineConfig } from "rolldown";
|
||||
|
||||
export default defineConfig({
|
||||
input: "src/index.ts",
|
||||
input: ["src/index.ts", "src/migrations.ts"],
|
||||
output: {
|
||||
format: "esm",
|
||||
dir: "dist",
|
||||
entryFileNames: "index.js",
|
||||
},
|
||||
external: (id: string) =>
|
||||
id === "drizzle-orm" ||
|
||||
|
|
|
|||
8
packages/schema/src/constants.ts
Normal file
8
packages/schema/src/constants.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const PROJECT_STATUSES = ["active", "archived", "completed"] as const;
|
||||
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
|
||||
|
||||
export const TASK_STATUSES = ["todo", "in_progress", "in_review", "done"] as const;
|
||||
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
||||
|
||||
export const PRIORITIES = ["low", "medium", "high", "urgent"] as const;
|
||||
export type Priority = (typeof PRIORITIES)[number];
|
||||
|
|
@ -2,9 +2,7 @@ import { pgTable, uuid, text, timestamp, check } from "drizzle-orm/pg-core";
|
|||
import { sql } from "drizzle-orm";
|
||||
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-valibot";
|
||||
import { InferOutput, picklist, pipe } from "valibot";
|
||||
|
||||
export const PROJECT_STATUSES = ["active", "archived", "completed"] as const;
|
||||
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
|
||||
import { PROJECT_STATUSES } from "../constants";
|
||||
|
||||
const projectStatusValues = PROJECT_STATUSES.map((s) => `'${s}'`).join(", ");
|
||||
const createProjectStatusSchema = (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import { sql } from "drizzle-orm";
|
|||
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-valibot";
|
||||
import { InferOutput, picklist, pipe } from "valibot";
|
||||
import { projects } from "./projects";
|
||||
|
||||
export const TASK_STATUSES = ["todo", "in_progress", "in_review", "done"] as const;
|
||||
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
||||
|
||||
export const PRIORITIES = ["low", "medium", "high", "urgent"] as const;
|
||||
export type Priority = (typeof PRIORITIES)[number];
|
||||
import { TASK_STATUSES, PRIORITIES } from "../constants";
|
||||
|
||||
const taskStatusValues = TASK_STATUSES.map((s) => `'${s}'`).join(", ");
|
||||
const priorityValues = PRIORITIES.map((p) => `'${p}'`).join(", ");
|
||||
|
|
|
|||
|
|
@ -1,34 +1,30 @@
|
|||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const migrationsDir = resolve(__dirname, "../drizzle");
|
||||
export {
|
||||
PROJECT_STATUSES,
|
||||
type ProjectStatus,
|
||||
TASK_STATUSES,
|
||||
type TaskStatus,
|
||||
PRIORITIES,
|
||||
type Priority,
|
||||
} from "./constants";
|
||||
|
||||
export { eq } from "drizzle-orm";
|
||||
export { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
export { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||
|
||||
export {
|
||||
PROJECT_STATUSES,
|
||||
projects,
|
||||
projectSelect,
|
||||
projectInsert,
|
||||
projectUpdate,
|
||||
type Project,
|
||||
type ProjectStatus,
|
||||
type ProjectInsertDTO,
|
||||
type ProjectUpdateDTO,
|
||||
} from "./drizzle/projects";
|
||||
export { projectsRelations, tasksRelations } from "./drizzle/relations";
|
||||
export {
|
||||
TASK_STATUSES,
|
||||
PRIORITIES,
|
||||
tasks,
|
||||
taskSelect,
|
||||
taskInsert,
|
||||
taskUpdate,
|
||||
type Task,
|
||||
type TaskStatus,
|
||||
type Priority,
|
||||
} from "./drizzle/tasks";
|
||||
|
|
|
|||
6
packages/schema/src/migrations.ts
Normal file
6
packages/schema/src/migrations.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const migrationsDir = resolve(__dirname, "../drizzle");
|
||||
Loading…
Reference in a new issue
Explore use of NuxtUI's UForm