diff --git a/bun.lockb b/bun.lockb index 37b9108..f9ae40a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 87c7b5f..054f9cf 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/bun": "^1.1.14", "autoprefixer": "^10.4.20", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", @@ -41,6 +42,7 @@ "prettier-plugin-tailwindcss": "^0.6.5", "prisma": "^6.0.1", "storybook": "^8.4.7", + "storybook-dark-mode": "^4.0.2", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^3.4.9", @@ -54,12 +56,11 @@ "@pothos/plugin-prisma": "^4.4.0", "@prisma/client": "6.0.1", "@tailwindcss/typography": "^0.5.15", - "@types/bun": "^1.1.14", + "dayjs": "^1.11.13", "graphql": "^16.9.0", "graphql-yoga": "^5.10.4", "pino": "^9.5.0", "pino-pretty": "^13.0.0", - "storybook-dark-mode": "^4.0.2", "zod": "^3.24.0" } } \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index 14dfc11..7886134 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal deleted file mode 100644 index be0024e..0000000 Binary files a/prisma/dev.db-journal and /dev/null differ diff --git a/prisma/migrations/20241211200947_add_timestamps/migration.sql b/prisma/migrations/20241211200947_add_timestamps/migration.sql new file mode 100644 index 0000000..d4aa687 --- /dev/null +++ b/prisma/migrations/20241211200947_add_timestamps/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - Made the column `content` on table `Post` required. This step will fail if there are existing NULL values in that column. + - Made the column `name` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "published" BOOLEAN DEFAULT false, + "authorId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Post" ("authorId", "content", "id", "published", "title") SELECT "authorId", "content", "id", "published", "title" FROM "Post"; +DROP TABLE "Post"; +ALTER TABLE "new_Post" RENAME TO "Post"; +CREATE TABLE "new_User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT, + "name" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_User" ("email", "id", "name") SELECT "email", "id", "name" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0cd53f5..c2faf75 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,17 +15,21 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - posts Post[] + id Int @id @default(autoincrement()) @unique + email String? @unique + name String + posts Post[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt } model Post { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) title String - content String? - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) + content String + published Boolean? @default(false) + author User @relation(fields: [authorId], references: [id]) authorId Int + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt } \ No newline at end of file diff --git a/src/lib/pothos/index.ts b/src/lib/pothos/index.ts deleted file mode 100644 index c8ee3d6..0000000 --- a/src/lib/pothos/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { prisma } from '$lib/prisma'; -import { Context } from '$lib/yoga/context'; -import SchemaBuilder from '@pothos/core'; -import PrismaPlugin, { type PrismaTypesFromClient } from '@pothos/plugin-prisma'; - -type ContextType = ReturnType; - -export const builder = new SchemaBuilder<{ - Context: ContextType; - PrismaTypes: PrismaTypesFromClient; -}>({ - plugins: [PrismaPlugin], - prisma: { - client: prisma, - // defaults to false, uses /// comments from prisma schema as descriptions - // for object types, relations and exposed fields. - // descriptions can be omitted by setting description to false - exposeDescriptions: false, - // use where clause from prismaRelatedConnection for totalCount (defaults to true) - filterConnectionTotalCount: true, - // warn when not using a query parameter correctly - onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn' - } -}); - -const User = builder.prismaObject('User', { - fields: (t) => ({ - id: t.exposeID('id'), - email: t.exposeString('email'), - name: t.exposeString('name'), - posts: t.relation('posts') - }) -}); - -const Post = builder.prismaObject('Post', { - fields: (t) => ({ - id: t.exposeID('id'), - title: t.exposeString('title'), - content: t.exposeString('content'), - published: t.exposeBoolean('published'), - author: t.relation('author') - }) -}); - -builder.queryType({ - fields: (t) => ({ - version: t.string({ - resolve: (parent, args, context) => context.config.app_version - }), - users: t.prismaField({ - type: [User], - resolve: async () => { - return await prisma.user.findMany(); - } - }), - posts: t.prismaField({ - type: [Post], - resolve: async () => { - return await prisma.post.findMany(); - } - }) - }) -}); - -export const Schema = builder.toSchema(); \ No newline at end of file diff --git a/src/lib/config/index.ts b/src/lib/server/config/index.ts similarity index 90% rename from src/lib/config/index.ts rename to src/lib/server/config/index.ts index b87d270..b7dcdbe 100644 --- a/src/lib/config/index.ts +++ b/src/lib/server/config/index.ts @@ -1,4 +1,4 @@ -import { logger } from '$lib/logger'; +import { logger } from '$lib/server/logger'; import { z } from 'zod'; export interface Configuration { diff --git a/src/lib/logger/index.ts b/src/lib/server/logger/index.ts similarity index 100% rename from src/lib/logger/index.ts rename to src/lib/server/logger/index.ts diff --git a/src/lib/server/pothos/builder.ts b/src/lib/server/pothos/builder.ts new file mode 100644 index 0000000..0b7fda4 --- /dev/null +++ b/src/lib/server/pothos/builder.ts @@ -0,0 +1,28 @@ +import { prisma } from '$lib/server/prisma'; +import type { Context } from '$lib/server/yoga'; +import SchemaBuilder from '@pothos/core'; +import PrismaPlugin, { type PrismaTypesFromClient } from '@pothos/plugin-prisma'; +import type { Scalars } from './schema/Scalars'; + +type PothosType = { + Context: ReturnType; + PrismaTypes: PrismaTypesFromClient; + Scalars: Scalars; +}; + +SchemaBuilder.allowPluginReRegistration = true; + +export const builder = new SchemaBuilder({ + plugins: [PrismaPlugin], + prisma: { + client: prisma, + // defaults to false, uses /// comments from prisma schema as descriptions + // for object types, relations and exposed fields. + // descriptions can be omitted by setting description to false + exposeDescriptions: false, + // use where clause from prismaRelatedConnection for totalCount (defaults to true) + filterConnectionTotalCount: true, + // warn when not using a query parameter correctly + onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn' + } +}); \ No newline at end of file diff --git a/src/lib/server/pothos/index.ts b/src/lib/server/pothos/index.ts new file mode 100644 index 0000000..5d5aa13 --- /dev/null +++ b/src/lib/server/pothos/index.ts @@ -0,0 +1 @@ +export * from './schema'; \ No newline at end of file diff --git a/src/lib/server/pothos/schema/Scalars/Date.ts b/src/lib/server/pothos/schema/Scalars/Date.ts new file mode 100644 index 0000000..6573f0a --- /dev/null +++ b/src/lib/server/pothos/schema/Scalars/Date.ts @@ -0,0 +1,14 @@ +import { builder } from '../../builder'; + +export const DateScalar = builder.scalarType('Date', { + description: 'Date Scalar in ISO format', + serialize: (date) => { + return date.toISOString(); + }, + parseValue: (date) => { + if (typeof date !== 'string') { + throw new Error('Cyka blyat'); + } + return new Date(date); + } +}); \ No newline at end of file diff --git a/src/lib/server/pothos/schema/Scalars/index.ts b/src/lib/server/pothos/schema/Scalars/index.ts new file mode 100644 index 0000000..0751f41 --- /dev/null +++ b/src/lib/server/pothos/schema/Scalars/index.ts @@ -0,0 +1,8 @@ +export * from './Date'; + +export type Scalars = { + Date: { + Input: Date; + Output: Date; + }; +}; \ No newline at end of file diff --git a/src/lib/server/pothos/schema/index.ts b/src/lib/server/pothos/schema/index.ts new file mode 100644 index 0000000..acdab44 --- /dev/null +++ b/src/lib/server/pothos/schema/index.ts @@ -0,0 +1,18 @@ +import { builder } from '../builder'; + +builder.queryType({}); + +builder.queryField('version', (t) => + t.string({ + description: 'Application version', + resolve: (parent, args, context) => context.config.app_version + }) +); + +builder.mutationType({}); + +import './Scalars'; +import './posts'; +import './users'; + +export const Schema = builder.toSchema(); \ No newline at end of file diff --git a/src/lib/server/pothos/schema/posts.ts b/src/lib/server/pothos/schema/posts.ts new file mode 100644 index 0000000..f616871 --- /dev/null +++ b/src/lib/server/pothos/schema/posts.ts @@ -0,0 +1,110 @@ +import { prisma } from '$lib/server/prisma'; +import { builder } from '../builder'; + +export const Post = builder.prismaObject('Post', { + fields: (t) => ({ + id: t.exposeID('id'), + title: t.exposeString('title'), + content: t.exposeString('content'), + published: t.exposeBoolean('published'), + author: t.relation('author'), + createdAt: t.expose('createdAt', { + type: 'Date' + }), + updatedAt: t.expose('updatedAt', { + type: 'Date' + }) + }) +}); + +const CreatePost = builder.inputType('CreatePost', { + fields: (t) => ({ + title: t.string({ + required: true + }), + content: t.string({ + required: true + }), + published: t.boolean(), + authorId: t.id({ + required: true + }) + }) +}); + +const UpdatePost = builder.inputType('UpdatePost', { + fields: (t) => ({ + id: t.id({ + required: true + }), + title: t.string(), + content: t.string(), + published: t.boolean(), + authorId: t.id() + }) +}); + +builder.queryFields((t) => ({ + posts: t.prismaField({ + type: [Post], + resolve: async () => { + return await prisma.post.findMany(); + } + }) +})); + +builder.mutationFields((t) => ({ + createPost: t.field({ + type: Post, + args: { + input: t.arg({ required: true, type: CreatePost }) + }, + resolve: async (parent, args) => { + const author = await prisma.user.findUnique({ + where: { id: Number(args.input.authorId) } + }); + if (!author) { + throw new Error('Author does not exist!'); + } + const post = await prisma.post.create({ + data: { + title: args.input.title, + content: args.input.content, + published: args.input.published, + author: { + connect: { + id: author.id + } + } + } + }); + return post; + } + }), + updatePost: t.field({ + type: Post, + args: { + input: t.arg({ required: true, type: UpdatePost }) + }, + resolve: async (parent, args) => { + const post = await prisma.post.update({ + where: { + id: Number(args.input.id) + }, + data: { + title: args.input.title ?? undefined, + content: args.input.content ?? undefined, + published: args.input.published, + ...(args.input.authorId && { + author: { + connect: { + id: Number(args.input.authorId) + } + } + }) + } + }); + return post; + } + }) +})); \ No newline at end of file diff --git a/src/lib/server/pothos/schema/users.ts b/src/lib/server/pothos/schema/users.ts new file mode 100644 index 0000000..c15b8bb --- /dev/null +++ b/src/lib/server/pothos/schema/users.ts @@ -0,0 +1,83 @@ +import { prisma } from '$lib/server/prisma'; +import { builder } from '../builder'; + +export const User = builder.prismaObject('User', { + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + name: t.exposeString('name'), + posts: t.relation('posts'), + createdAt: t.expose('createdAt', { + type: 'Date' + }), + updatedAt: t.expose('updatedAt', { + type: 'Date' + }) + }) +}); + +const CreateUser = builder.inputType('CreateUser', { + fields: (t) => ({ + email: t.string({ + required: true + }), + name: t.string({ + required: true + }) + }) +}); + +const UpdateUser = builder.inputType('UpdateUser', { + fields: (t) => ({ + id: t.id({ + required: true + }), + email: t.string(), + name: t.string() + }) +}); + +builder.queryFields((t) => ({ + users: t.prismaField({ + type: [User], + resolve: async () => { + return await prisma.user.findMany(); + } + }) +})); + +builder.mutationFields((t) => ({ + createUser: t.field({ + type: User, + args: { + input: t.arg({ required: true, type: CreateUser }) + }, + resolve: async (parent, args) => { + const post = await prisma.user.create({ + data: { + email: args.input.email, + name: args.input.name + } + }); + return post; + } + }), + updateUser: t.field({ + type: User, + args: { + input: t.arg({ required: true, type: UpdateUser }) + }, + resolve: async (parent, args) => { + const post = await prisma.user.update({ + where: { + id: Number(args.input.id) + }, + data: { + email: args.input.email, + name: args.input.name ?? undefined + } + }); + return post; + } + }) +})); \ No newline at end of file diff --git a/src/lib/prisma/index.ts b/src/lib/server/prisma/index.ts similarity index 100% rename from src/lib/prisma/index.ts rename to src/lib/server/prisma/index.ts diff --git a/src/lib/yoga/context.ts b/src/lib/server/yoga/context.ts similarity index 78% rename from src/lib/yoga/context.ts rename to src/lib/server/yoga/context.ts index d9a6a68..8a3ab51 100644 --- a/src/lib/yoga/context.ts +++ b/src/lib/server/yoga/context.ts @@ -1,4 +1,4 @@ -import { Config } from '$lib/config'; +import { Config } from '$lib/server/config'; import type { YogaInitialContext } from 'graphql-yoga'; export const Context = (initialContext: YogaInitialContext) => ({ diff --git a/src/lib/server/yoga/index.ts b/src/lib/server/yoga/index.ts new file mode 100644 index 0000000..b489ffb --- /dev/null +++ b/src/lib/server/yoga/index.ts @@ -0,0 +1,2 @@ +export * from './context'; +export * from './server'; \ No newline at end of file diff --git a/src/lib/yoga/index.ts b/src/lib/server/yoga/server.ts similarity index 78% rename from src/lib/yoga/index.ts rename to src/lib/server/yoga/server.ts index b008b24..4bdfbe6 100644 --- a/src/lib/yoga/index.ts +++ b/src/lib/server/yoga/server.ts @@ -1,5 +1,5 @@ -import { yogaLogger } from '$lib/logger'; -import { Schema } from '$lib/pothos'; +import { yogaLogger } from '$lib/server/logger'; +import { Schema } from '$lib/server/pothos'; import type { RequestEvent } from '@sveltejs/kit'; import { createYoga } from 'graphql-yoga'; import { Context } from './context'; diff --git a/src/routes/api/graphql/+server.ts b/src/routes/api/graphql/+server.ts index 72aab8c..c208143 100644 --- a/src/routes/api/graphql/+server.ts +++ b/src/routes/api/graphql/+server.ts @@ -1,3 +1,3 @@ -import { Yoga } from '$lib/yoga'; +import { Yoga } from '$lib/server/yoga'; export { Yoga as GET, Yoga as POST }; \ No newline at end of file