diff --git a/examples/cattery-valibot/package.json b/examples/cattery-valibot/package.json index 282934f8..1a5e1edb 100644 --- a/examples/cattery-valibot/package.json +++ b/examples/cattery-valibot/package.json @@ -11,7 +11,8 @@ "description": "", "devDependencies": { "@types/node": "^22.13.4", - "drizzle-kit": "^0.30.1", + "drizzle-kit": "1.0.0-beta.1-7946562", + "drizzle-orm": "1.0.0-beta.1-7946562", "tsx": "^4.7.2", "typescript": "^5.7.3" }, @@ -21,8 +22,8 @@ "@gqloom/valibot": "latest", "@libsql/client": "^0.14.0", "dotenv": "^16.4.7", - "drizzle-orm": "^0.39.3", "graphql": "^16.8.1", + "graphql-scalars": "^1.24.1", "graphql-yoga": "^5.6.0", "valibot": "1.0.0-rc.1" } diff --git a/examples/cattery-valibot/schema.graphql b/examples/cattery-valibot/schema.graphql index 37122c79..c26cee4e 100644 --- a/examples/cattery-valibot/schema.graphql +++ b/examples/cattery-valibot/schema.graphql @@ -8,12 +8,17 @@ type UsersItem { type CatsItem { id: Int! name: String! - birthday: String! + birthday: DateTime! ownerId: Int! age(currentYear: Int): Float! owner: UsersItem } +""" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar DateTime + type Query { hello(name: String): String! mine: UsersItem! @@ -22,7 +27,7 @@ type Query { cats( offset: Int limit: Int - orderBy: [CatsOrderBy!] + orderBy: CatsOrderBy where: CatsFilters ): [CatsItem!]! } @@ -44,7 +49,9 @@ input CatsFilters { name: SQLiteTextFilters birthday: SQLiteTimestampFilters ownerId: SQLiteIntegerFilters - OR: [CatsFiltersOr!] + OR: [CatsFiltersNested!] + AND: [CatsFiltersNested!] + NOT: CatsFiltersNested } input SQLiteIntegerFilters { @@ -54,22 +61,24 @@ input SQLiteIntegerFilters { lte: Int gt: Int gte: Int - inArray: [Int!] - notInArray: [Int!] + in: [Int!] + notIn: [Int!] isNull: Boolean isNotNull: Boolean - OR: [SQLiteIntegerFiltersOr!] + OR: [SQLiteIntegerFiltersNested!] + AND: [SQLiteIntegerFiltersNested!] + NOT: SQLiteIntegerFiltersNested } -input SQLiteIntegerFiltersOr { +input SQLiteIntegerFiltersNested { eq: Int ne: Int lt: Int lte: Int gt: Int gte: Int - inArray: [Int!] - notInArray: [Int!] + in: [Int!] + notIn: [Int!] isNull: Boolean isNotNull: Boolean } @@ -85,14 +94,16 @@ input SQLiteTextFilters { notLike: String ilike: String notIlike: String - inArray: [String!] - notInArray: [String!] + in: [String!] + notIn: [String!] isNull: Boolean isNotNull: Boolean - OR: [SQLiteTextFiltersOr!] + OR: [SQLiteTextFiltersNested!] + AND: [SQLiteTextFiltersNested!] + NOT: SQLiteTextFiltersNested } -input SQLiteTextFiltersOr { +input SQLiteTextFiltersNested { eq: String ne: String lt: String @@ -103,48 +114,42 @@ input SQLiteTextFiltersOr { notLike: String ilike: String notIlike: String - inArray: [String!] - notInArray: [String!] + in: [String!] + notIn: [String!] isNull: Boolean isNotNull: Boolean } input SQLiteTimestampFilters { - eq: String - ne: String - lt: String - lte: String - gt: String - gte: String - like: String - notLike: String - ilike: String - notIlike: String - inArray: [String!] - notInArray: [String!] + eq: DateTime + ne: DateTime + lt: DateTime + lte: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + notIn: [DateTime!] isNull: Boolean isNotNull: Boolean - OR: [SQLiteTimestampFiltersOr!] + OR: [SQLiteTimestampFiltersNested!] + AND: [SQLiteTimestampFiltersNested!] + NOT: SQLiteTimestampFiltersNested } -input SQLiteTimestampFiltersOr { - eq: String - ne: String - lt: String - lte: String - gt: String - gte: String - like: String - notLike: String - ilike: String - notIlike: String - inArray: [String!] - notInArray: [String!] +input SQLiteTimestampFiltersNested { + eq: DateTime + ne: DateTime + lt: DateTime + lte: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + notIn: [DateTime!] isNull: Boolean isNotNull: Boolean } -input CatsFiltersOr { +input CatsFiltersNested { id: SQLiteIntegerFilters name: SQLiteTextFilters birthday: SQLiteTimestampFilters @@ -153,7 +158,12 @@ input CatsFiltersOr { type Mutation { createUser(data: CreateUserDataInput!): UsersItem! - createCats(values: [CreateCatsValuesInput!]!): [CatsItem!]! + insertCats( + values: [CatsInsertInput!]! + onConflictDoUpdate: CatsInsertOnConflictDoUpdateInput + onConflictDoNothing: CatsInsertOnConflictDoNothingInput + ): [CatsItem!]! + createManyCats(values: [CreateManyCatsValuesInput!]!): [CatsItem!]! } input CreateUserDataInput { @@ -161,7 +171,33 @@ input CreateUserDataInput { phone: String! } -input CreateCatsValuesInput { +input CatsInsertInput { + id: Int + name: String! + birthday: DateTime! + ownerId: Int! +} + +input CatsInsertOnConflictDoUpdateInput { + target: [CatsTableColumn!]! + set: CatsInsertInput + targetWhere: CatsFilters + setWhere: CatsFilters +} + +enum CatsTableColumn { + id + name + birthday + ownerId +} + +input CatsInsertOnConflictDoNothingInput { + target: [CatsTableColumn!] + where: CatsFilters +} + +input CreateManyCatsValuesInput { name: String! birthday: String! } diff --git a/examples/cattery-valibot/src/contexts/index.ts b/examples/cattery-valibot/src/contexts/index.ts index 49a76619..57d6d3d5 100644 --- a/examples/cattery-valibot/src/contexts/index.ts +++ b/examples/cattery-valibot/src/contexts/index.ts @@ -1,16 +1,14 @@ import { createMemoization, useContext } from "@gqloom/core/context" -import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) if (user == null) throw new GraphQLError("Unauthorized") return user }) diff --git a/examples/cattery-valibot/src/index.ts b/examples/cattery-valibot/src/index.ts index 4efc8cb8..fd83b97d 100644 --- a/examples/cattery-valibot/src/index.ts +++ b/examples/cattery-valibot/src/index.ts @@ -1,10 +1,40 @@ import { createServer } from "node:http" -import { weave } from "@gqloom/core" +import { type Middleware, weave } from "@gqloom/core" +import { asyncContextProvider } from "@gqloom/core/context" import { ValibotWeaver } from "@gqloom/valibot" +import { GraphQLDateTime, GraphQLJSONObject } from "graphql-scalars" import { createYoga } from "graphql-yoga" import { resolvers } from "./resolvers" -const schema = weave(asyncContextProvider, ValibotWeaver, ...resolvers) +const exceptionFilter: Middleware = async (next) => { + try { + return await next() + } catch (error) { + // biome-ignore lint/suspicious/noConsole: log error + console.error(error) + if (error instanceof Error) { + throw new GraphQLError(error.message) + } + throw new GraphQLError("There has been something wrong...") + } +} + +const schema = weave( + asyncContextProvider, + ValibotWeaver, + ...resolvers, + exceptionFilter, + DrizzleWeaver.config({ + presetGraphQLType(column) { + if (column.dataType === "date") { + return GraphQLDateTime + } + if (column.dataType === "json") { + return GraphQLJSONObject + } + }, + }) +) const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { @@ -13,8 +43,8 @@ createServer(yoga).listen(4000, () => { import * as fs from "fs" import * as path from "path" -import { asyncContextProvider } from "@gqloom/core/context" -import { printSchema } from "graphql" +import { DrizzleWeaver } from "@gqloom/drizzle" +import { GraphQLError, printSchema } from "graphql" if (process.env.NODE_ENV !== "production") { fs.writeFileSync( path.resolve(__dirname, "../schema.graphql"), diff --git a/examples/cattery-valibot/src/providers/index.ts b/examples/cattery-valibot/src/providers/index.ts index 3002ff0f..945cbc6b 100644 --- a/examples/cattery-valibot/src/providers/index.ts +++ b/examples/cattery-valibot/src/providers/index.ts @@ -1,6 +1,6 @@ import { drizzle } from "drizzle-orm/libsql" -import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { - schema, + relations, }) diff --git a/examples/cattery-valibot/src/resolvers/cat.ts b/examples/cattery-valibot/src/resolvers/cat.ts index 9a940bb2..3e07829d 100644 --- a/examples/cattery-valibot/src/resolvers/cat.ts +++ b/examples/cattery-valibot/src/resolvers/cat.ts @@ -5,7 +5,7 @@ import { useCurrentUser } from "../contexts" import { db } from "../providers" import { cats } from "../schema" -const catResolverFactory = drizzleResolverFactory(db, "cats") +const catResolverFactory = drizzleResolverFactory(db, cats) export const catResolver = resolver.of(cats, { cats: catResolverFactory.selectArrayQuery(), @@ -23,7 +23,9 @@ export const catResolver = resolver.of(cats, { owner: catResolverFactory.relationField("owner"), - createCats: catResolverFactory.insertArrayMutation({ + insertCats: catResolverFactory.insertArrayMutation(), + + createManyCats: catResolverFactory.insertArrayMutation({ input: v.pipeAsync( v.objectAsync({ values: v.arrayAsync( diff --git a/examples/cattery-valibot/src/resolvers/user.ts b/examples/cattery-valibot/src/resolvers/user.ts index d5df2e7a..e880b7a9 100644 --- a/examples/cattery-valibot/src/resolvers/user.ts +++ b/examples/cattery-valibot/src/resolvers/user.ts @@ -1,12 +1,11 @@ import { mutation, query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" -import { eq } from "drizzle-orm" import * as v from "valibot" import { useCurrentUser } from "../contexts" import { db } from "../providers" import { users } from "../schema" -const userResolverFactory = drizzleResolverFactory(db, "users") +const userResolverFactory = drizzleResolverFactory(db, users) export const userResolver = resolver.of(users, { cats: userResolverFactory.relationField("cats"), @@ -15,19 +14,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ diff --git a/examples/cattery-valibot/src/schema/index.ts b/examples/cattery-valibot/src/schema/index.ts index d4140ec5..65206df6 100644 --- a/examples/cattery-valibot/src/schema/index.ts +++ b/examples/cattery-valibot/src/schema/index.ts @@ -1,5 +1,4 @@ import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -10,10 +9,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( sqliteTable("cats", { id: integer().primaryKey({ autoIncrement: true }), @@ -24,10 +19,3 @@ export const cats = drizzleSilk( .references(() => users.id), }) ) - -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), -})) diff --git a/examples/cattery-valibot/src/schema/relations.ts b/examples/cattery-valibot/src/schema/relations.ts new file mode 100644 index 00000000..53b9fd7f --- /dev/null +++ b/examples/cattery-valibot/src/schema/relations.ts @@ -0,0 +1,14 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" + +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, +})) diff --git a/examples/cattery-zod/package.json b/examples/cattery-zod/package.json index 6f25983b..a542a4b2 100644 --- a/examples/cattery-zod/package.json +++ b/examples/cattery-zod/package.json @@ -11,7 +11,8 @@ "description": "", "devDependencies": { "@types/node": "^22.13.4", - "drizzle-kit": "^0.30.1", + "drizzle-kit": "1.0.0-beta.1-7946562", + "drizzle-orm": "1.0.0-beta.1-7946562", "tsx": "^4.7.2", "typescript": "^5.7.3" }, @@ -21,7 +22,6 @@ "@gqloom/zod": "latest", "@libsql/client": "^0.14.0", "dotenv": "^16.4.7", - "drizzle-orm": "^0.39.3", "graphql": "^16.8.1", "graphql-yoga": "^5.6.0", "zod": "4.0.0-beta.20250415T232143" diff --git a/examples/cattery-zod/src/contexts/index.ts b/examples/cattery-zod/src/contexts/index.ts index ad695423..682e98b3 100644 --- a/examples/cattery-zod/src/contexts/index.ts +++ b/examples/cattery-zod/src/contexts/index.ts @@ -1,18 +1,15 @@ import { createMemoization, useContext } from "@gqloom/core/context" -import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ - where: eq(users.phone, phone), - }) + const user = await db.query.users.findFirst({ where: { phone } }) + if (user == null) throw new GraphQLError("Unauthorized") return user }) diff --git a/examples/cattery-zod/src/providers/index.ts b/examples/cattery-zod/src/providers/index.ts index 3002ff0f..a736f276 100644 --- a/examples/cattery-zod/src/providers/index.ts +++ b/examples/cattery-zod/src/providers/index.ts @@ -1,6 +1,8 @@ import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) diff --git a/examples/cattery-zod/src/resolvers/user.ts b/examples/cattery-zod/src/resolvers/user.ts index b2a0220d..c1d604d6 100644 --- a/examples/cattery-zod/src/resolvers/user.ts +++ b/examples/cattery-zod/src/resolvers/user.ts @@ -1,6 +1,5 @@ import { mutation, query, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" -import { eq } from "drizzle-orm" import * as z from "zod" import { useCurrentUser } from "../contexts" import { db } from "../providers" @@ -15,19 +14,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ diff --git a/examples/cattery-zod/src/schema/index.ts b/examples/cattery-zod/src/schema/index.ts index d4140ec5..65206df6 100644 --- a/examples/cattery-zod/src/schema/index.ts +++ b/examples/cattery-zod/src/schema/index.ts @@ -1,5 +1,4 @@ import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -10,10 +9,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( sqliteTable("cats", { id: integer().primaryKey({ autoIncrement: true }), @@ -24,10 +19,3 @@ export const cats = drizzleSilk( .references(() => users.id), }) ) - -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), -})) diff --git a/examples/cattery-zod/src/schema/relations.ts b/examples/cattery-zod/src/schema/relations.ts new file mode 100644 index 00000000..53b9fd7f --- /dev/null +++ b/examples/cattery-zod/src/schema/relations.ts @@ -0,0 +1,14 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" + +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, +})) diff --git a/examples/drizzle/package.json b/examples/drizzle/package.json index e6ebe90e..48f683de 100644 --- a/examples/drizzle/package.json +++ b/examples/drizzle/package.json @@ -15,8 +15,8 @@ "devDependencies": { "@types/node": "^22.13.1", "@types/pg": "^8.11.10", - "drizzle-kit": "^0.30.1", - "drizzle-orm": "^0.39.3", + "drizzle-kit": "1.0.0-beta.1-7946562", + "drizzle-orm": "1.0.0-beta.1-7946562", "tsx": "^4.7.2", "typescript": "^5.7.3" }, diff --git a/examples/drizzle/src/relations.ts b/examples/drizzle/src/relations.ts new file mode 100644 index 00000000..5d67997a --- /dev/null +++ b/examples/drizzle/src/relations.ts @@ -0,0 +1,14 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./schema" + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, +})) diff --git a/examples/drizzle/src/schema.ts b/examples/drizzle/src/schema.ts index c30ca21e..09a22422 100644 --- a/examples/drizzle/src/schema.ts +++ b/examples/drizzle/src/schema.ts @@ -1,5 +1,4 @@ import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -14,10 +13,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), @@ -31,7 +26,3 @@ export const posts = drizzleSilk( authorId: t.integer().notNull(), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), -})) diff --git a/packages/drizzle/CHANGELOG.md b/packages/drizzle/CHANGELOG.md index 5540eb5b..00652922 100644 --- a/packages/drizzle/CHANGELOG.md +++ b/packages/drizzle/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## Next (YYYY-MM-DD) + +- Refactor: Migrating to Relational Queries version 2 + ## 0.9.2 (2025-05-13) - Fix: DrizzleInputFactory to support custom column types diff --git a/packages/drizzle/drizzle-mysql.config.ts b/packages/drizzle/drizzle-mysql.config.ts index ab4dca43..6b9cfe69 100644 --- a/packages/drizzle/drizzle-mysql.config.ts +++ b/packages/drizzle/drizzle-mysql.config.ts @@ -7,5 +7,5 @@ export default defineConfig({ dbCredentials: { url: config.mysqlUrl, }, - tablesFilter: ["drizzle_user", "drizzle_post"], + tablesFilter: ["users", "posts"], }) diff --git a/packages/drizzle/drizzle-postgres.config.ts b/packages/drizzle/drizzle-postgres.config.ts index d9f1b987..98db76c3 100644 --- a/packages/drizzle/drizzle-postgres.config.ts +++ b/packages/drizzle/drizzle-postgres.config.ts @@ -8,5 +8,5 @@ export default defineConfig({ dbCredentials: { url: config.postgresUrl, }, - tablesFilter: ["drizzle_user", "drizzle_post"], + tablesFilter: ["users", "posts"], }) diff --git a/packages/drizzle/drizzle-sqlite-1.config.ts b/packages/drizzle/drizzle-sqlite-1.config.ts index 6f2eda8d..32a4791f 100644 --- a/packages/drizzle/drizzle-sqlite-1.config.ts +++ b/packages/drizzle/drizzle-sqlite-1.config.ts @@ -7,11 +7,10 @@ export default defineConfig({ url: "file:./test/schema/sqlite-1.db", }, tablesFilter: [ - "user", - "post", - "course", - "studentToCourse", - "studentCourseGrade", - "studentCourseGrade", + "users", + "posts", + "courses", + "studentToCourses", + "studentCourseGrades", ], }) diff --git a/packages/drizzle/drizzle-sqlite.config.ts b/packages/drizzle/drizzle-sqlite.config.ts index 3eddced3..6531b33b 100644 --- a/packages/drizzle/drizzle-sqlite.config.ts +++ b/packages/drizzle/drizzle-sqlite.config.ts @@ -7,10 +7,10 @@ export default defineConfig({ url: "file:./test/schema/sqlite.db", }, tablesFilter: [ - "user", - "post", - "course", - "studentToCourse", - "studentCourseGrade", + "users", + "posts", + "courses", + "studentToCourses", + "studentCourseGrades", ], }) diff --git a/packages/drizzle/package.json b/packages/drizzle/package.json index 5d6a898a..16b64625 100644 --- a/packages/drizzle/package.json +++ b/packages/drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@gqloom/drizzle", - "version": "0.10.0", + "version": "0.11.0-beta.0", "description": "GQLoom integration with Drizzle ORM", "type": "module", "main": "./dist/index.js", @@ -45,7 +45,7 @@ "license": "MIT", "peerDependencies": { "@gqloom/core": ">= 0.10.0", - "drizzle-orm": ">= 0.38.0", + "drizzle-orm": ">= 1.0.0-beta.1", "graphql": ">= 16.8.0" }, "devDependencies": { @@ -54,11 +54,11 @@ "@libsql/client": "^0.14.0", "@types/pg": "^8.11.10", "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.1", - "drizzle-orm": "^0.39.3", + "drizzle-kit": "1.0.0-beta.1-7946562", + "drizzle-orm": "1.0.0-beta.1-7946562", "graphql": "^16.8.1", "graphql-yoga": "^5.6.0", - "mysql2": "^3.12.0", + "mysql2": "^3.14.0", "pg": "^8.13.1", "tsx": "^4.7.2", "valibot": "1.0.0-beta.12" diff --git a/packages/drizzle/src/factory/index.ts b/packages/drizzle/src/factory/index.ts index 1cc0544c..fd2a6509 100644 --- a/packages/drizzle/src/factory/index.ts +++ b/packages/drizzle/src/factory/index.ts @@ -8,8 +8,37 @@ import { DrizzlePostgresResolverFactory } from "./resolver-postgres" import { DrizzleSQLiteResolverFactory } from "./resolver-sqlite" import type { BaseDatabase } from "./types" +/** + * Create a resolver factory for SQLite databases. + * + * @param db - The SQLite database instance. + * @param table - The table to create a resolver factory for. + * @param options - The options for the resolver factory. + */ export function drizzleResolverFactory< - TDatabase extends BaseSQLiteDatabase, + TDatabase extends BaseSQLiteDatabase, + TTable extends SQLiteTable, +>( + db: TDatabase, + table: TTable, + options?: DrizzleResolverFactoryOptions +): DrizzleSQLiteResolverFactory + +/** + * @deprecated directly use `table` instead of `tableName`. + * + * ## Example + * ⛔️ Don't do this + * ```ts + * const userFactory = drizzleResolverFactory(db, "users") + * ``` + * ✅ Do this + * ```ts + * const userFactory = drizzleResolverFactory(db, users) + * ``` + */ +export function drizzleResolverFactory< + TDatabase extends BaseSQLiteDatabase, TTableName extends keyof NonNullable, >( db: TDatabase, @@ -21,17 +50,54 @@ export function drizzleResolverFactory< TDatabase, NonNullable[TTableName] > + +/** + * Create a resolver factory for PostgreSQL databases. + * + * @param db - The PostgreSQL database instance. + * @param table - The table to create a resolver factory for. + * @param options - The options for the resolver factory. + */ export function drizzleResolverFactory< - TDatabase extends BaseSQLiteDatabase, - TTable extends SQLiteTable, + TDatabase extends PgDatabase, + TTable extends PgTable, >( db: TDatabase, table: TTable, options?: DrizzleResolverFactoryOptions -): DrizzleSQLiteResolverFactory +): DrizzlePostgresResolverFactory + +/** + * Create a resolver factory for MySQL databases. + * + * @param db - The MySQL database instance. + * @param table - The table to create a resolver factory for. + * @param options - The options for the resolver factory. + */ +export function drizzleResolverFactory< + TDatabase extends MySqlDatabase, + TTable extends MySqlTable, +>( + db: TDatabase, + table: TTable, + options?: DrizzleResolverFactoryOptions +): DrizzleMySQLResolverFactory +/** + * @deprecated directly use `table` instead of `tableName`. + * + * ## Example + * ⛔️ Don't do this + * ```ts + * const userFactory = drizzleResolverFactory(db, "users") + * ``` + * ✅ Do this + * ```ts + * const userFactory = drizzleResolverFactory(db, users) + * ``` + */ export function drizzleResolverFactory< - TDatabase extends PgDatabase, + TDatabase extends PgDatabase, TTableName extends keyof NonNullable, >( db: TDatabase, @@ -43,17 +109,23 @@ export function drizzleResolverFactory< TDatabase, NonNullable[TTableName] > -export function drizzleResolverFactory< - TDatabase extends PgDatabase, - TTable extends PgTable, ->( - db: TDatabase, - table: TTable, - options?: DrizzleResolverFactoryOptions -): DrizzlePostgresResolverFactory + +/** + * @deprecated directly use `table` instead of `tableName`. + * + * ## Example + * ⛔️ Don't do this + * ```ts + * const userFactory = drizzleResolverFactory(db, "users") + * ``` + * ✅ Do this + * ```ts + * const userFactory = drizzleResolverFactory(db, users) + * ``` + */ export function drizzleResolverFactory< - TDatabase extends MySqlDatabase, + TDatabase extends MySqlDatabase, TTableName extends keyof NonNullable, >( db: TDatabase, @@ -65,14 +137,6 @@ export function drizzleResolverFactory< TDatabase, NonNullable[TTableName] > -export function drizzleResolverFactory< - TDatabase extends MySqlDatabase, - TTable extends MySqlTable, ->( - db: TDatabase, - table: TTable, - options?: DrizzleResolverFactoryOptions -): DrizzleMySQLResolverFactory export function drizzleResolverFactory( db: BaseDatabase, diff --git a/packages/drizzle/src/factory/input.ts b/packages/drizzle/src/factory/input.ts index 8b10cb17..687cf395 100644 --- a/packages/drizzle/src/factory/input.ts +++ b/packages/drizzle/src/factory/input.ts @@ -1,8 +1,10 @@ import { SYMBOLS, mapValue, pascalCase, weaverContext } from "@gqloom/core" +import { getGraphQLType } from "@gqloom/core" import { type Column, type InferInsertModel, type InferSelectModel, + type Many, type Table, getTableColumns, getTableName, @@ -19,8 +21,13 @@ import { isNonNullType, } from "graphql" import { getValue, isColumnVisible } from "../helper" -import { DrizzleWeaver } from "../index" +import { type BaseDatabase, DrizzleWeaver } from "../index" import type { DrizzleResolverFactoryOptions, DrizzleSilkConfig } from "../types" +import type { + InferRelationTable, + InferTableRelationalConfig, + QueryBuilder, +} from "./types" export class DrizzleInputFactory { public constructor( @@ -40,7 +47,7 @@ export class DrizzleInputFactory { offset: { type: GraphQLInt }, limit: { type: GraphQLInt }, orderBy: { - type: new GraphQLList(new GraphQLNonNull(this.orderBy())), + type: this.orderBy(), }, where: { type: this.filters() }, }, @@ -59,7 +66,7 @@ export class DrizzleInputFactory { fields: { offset: { type: GraphQLInt }, orderBy: { - type: new GraphQLList(new GraphQLNonNull(this.orderBy())), + type: this.orderBy(), }, where: { type: this.filters() }, }, @@ -82,6 +89,39 @@ export class DrizzleInputFactory { ) } + public relationToManyArgs() { + const name = `${pascalCase(getTableName(this.table))}RelationToManyArgs` + const existing = weaverContext.getNamedType(name) as GraphQLObjectType + if (existing != null) return existing + + return weaverContext.memoNamedType( + new GraphQLObjectType>({ + name, + fields: { + where: { type: this.filters() }, + orderBy: { type: this.orderBy() }, + limit: { type: GraphQLInt }, + offset: { type: GraphQLInt }, + }, + }) + ) + } + + public relationToOneArgs() { + const name = `${pascalCase(getTableName(this.table))}RelationToOneArgs` + const existing = weaverContext.getNamedType(name) as GraphQLObjectType + if (existing != null) return existing + + return weaverContext.memoNamedType( + new GraphQLObjectType>({ + name, + fields: { + where: { type: this.filters() }, + }, + }) + ) + } + public insertArrayArgs() { const name = `${pascalCase(getTableName(this.table))}InsertArrayArgs` const existing = weaverContext.getNamedType(name) as GraphQLObjectType @@ -207,6 +247,30 @@ export class DrizzleInputFactory { ) } + protected getColumnInputType( + key: string, + mutation: "insert" | "update", + column: Column, + columnConfig: ReturnType + ) { + const colSilk = (() => { + const behavior = this.options?.input[key] + if (typeof behavior != "object" || behavior == null) return undefined + if ("~standard" in behavior) { + return behavior + } + const mutationConfigBehavior = behavior[mutation] + if ( + typeof mutationConfigBehavior != "object" || + mutationConfigBehavior == null + ) + return undefined + return mutationConfigBehavior + })() + if (colSilk != null) return getGraphQLType(colSilk) + return getValue(columnConfig?.type) || DrizzleWeaver.getColumnType(column) + } + public insertInput() { const tableConfig = DrizzleWeaver.silkConfigs.get(this.table) const tableName = tableConfig?.name ?? getTableName(this.table) @@ -220,25 +284,21 @@ export class DrizzleInputFactory { new GraphQLObjectType({ name, description: tableConfig?.description, - fields: mapValue(columns, (column, columnName) => { - if ( - !isColumnVisible(columnName, this.options?.input ?? {}, "insert") - ) { + fields: mapValue(columns, (column, key) => { + if (!isColumnVisible(key, this.options?.input ?? {}, "insert")) { return mapValue.SKIP } const fieldConfig = DrizzleInputFactory.getColumnConfig( tableConfig, - columnName + key ) - const type = (() => { - const t = - getValue(fieldConfig?.type) || DrizzleWeaver.getColumnType(column) - if (column.hasDefault) return t - if (column.notNull && !isNonNullType(t)) - return new GraphQLNonNull(t) - return t - })() + let type = this.getColumnInputType(key, "insert", column, fieldConfig) + const isNotNull = + !column.hasDefault && column.notNull && !isNonNullType(type) + if (isNotNull) { + type = new GraphQLNonNull(type) + } return { type, description: fieldConfig?.description } }), }) @@ -304,18 +364,23 @@ export class DrizzleInputFactory { new GraphQLObjectType({ name, description: tableConfig?.description, - fields: mapValue(columns, (column, columnName) => { - if ( - !isColumnVisible(columnName, this.options?.input ?? {}, "update") - ) { + fields: mapValue(columns, (column, key) => { + if (!isColumnVisible(key, this.options?.input ?? {}, "update")) { return mapValue.SKIP } const columnConfig = DrizzleInputFactory.getColumnConfig( tableConfig, - columnName + key + ) + let type = this.getColumnInputType( + key, + "update", + column, + columnConfig ) - const type = - getValue(columnConfig?.type) || DrizzleWeaver.getColumnType(column) + if (type instanceof GraphQLNonNull) { + type = type.ofType + } return { type, description: columnConfig?.description } }), }) @@ -351,8 +416,8 @@ export class DrizzleInputFactory { } }) - const filtersOr = new GraphQLObjectType({ - name: `${pascalCase(tableName)}FiltersOr`, + const filtersNested = new GraphQLObjectType({ + name: `${pascalCase(tableName)}FiltersNested`, fields: { ...filterFields }, }) return weaverContext.memoNamedType( @@ -361,7 +426,9 @@ export class DrizzleInputFactory { description: tableConfig?.description, fields: { ...filterFields, - OR: { type: new GraphQLList(new GraphQLNonNull(filtersOr)) }, + OR: { type: new GraphQLList(new GraphQLNonNull(filtersNested)) }, + AND: { type: new GraphQLList(new GraphQLNonNull(filtersNested)) }, + NOT: { type: filtersNested }, }, }) ) @@ -387,14 +454,14 @@ export class DrizzleInputFactory { ilike: { type: GraphQLString }, notIlike: { type: GraphQLString }, }), - inArray: { type: gqlListType }, - notInArray: { type: gqlListType }, + in: { type: gqlListType }, + notIn: { type: gqlListType }, isNull: { type: GraphQLBoolean }, isNotNull: { type: GraphQLBoolean }, } - const filtersOr = new GraphQLObjectType({ - name: `${pascalCase(column.columnType)}FiltersOr`, + const filtersNested = new GraphQLObjectType({ + name: `${pascalCase(column.columnType)}FiltersNested`, fields: { ...baseFields }, }) @@ -403,7 +470,9 @@ export class DrizzleInputFactory { name, fields: { ...baseFields, - OR: { type: new GraphQLList(new GraphQLNonNull(filtersOr)) }, + OR: { type: new GraphQLList(new GraphQLNonNull(filtersNested)) }, + AND: { type: new GraphQLList(new GraphQLNonNull(filtersNested)) }, + NOT: { type: filtersNested }, }, }) ) @@ -477,13 +546,13 @@ export class DrizzleInputFactory { export interface SelectArrayArgs { offset?: number limit?: number - orderBy?: Partial, "asc" | "desc">>[] + orderBy?: Partial, "asc" | "desc">> where?: Filters } export interface SelectSingleArgs { offset?: number - orderBy?: Partial, "asc" | "desc">>[] + orderBy?: Partial, "asc" | "desc">> where?: Filters } @@ -491,30 +560,46 @@ export interface CountArgs { where?: Filters } +export type RelationArgs< + TDatabase extends BaseDatabase, + TTable extends Table, + TRelationName extends keyof InferTableRelationalConfig< + QueryBuilder + >["relations"], +> = InferTableRelationalConfig< + QueryBuilder +>["relations"][TRelationName] extends Many + ? RelationToManyArgs> + : RelationToOneArgs> + +export interface RelationToManyArgs { + where?: Filters + orderBy?: Partial, "asc" | "desc">> + limit?: number + offset?: number +} + +export interface RelationToOneArgs { + where?: Filters +} + export interface InsertArrayArgs { values: InferInsertModel[] } export interface InsertArrayWithOnConflictArgs - extends InsertArrayArgs { - onConflictDoUpdate?: { - target: string[] - set?: Partial> - targetWhere?: Filters - setWhere?: Filters - } - onConflictDoNothing?: { - target?: string[] - where?: Filters - } -} + extends InsertArrayArgs, + InsertOnConflictInputArgs {} export interface InsertSingleArgs { value: InferInsertModel } export interface InsertSingleWithOnConflictArgs - extends InsertSingleArgs { + extends InsertSingleArgs, + InsertOnConflictInputArgs {} + +export interface InsertOnConflictInputArgs { onConflictDoUpdate?: { target: string[] set?: Partial> @@ -526,6 +611,7 @@ export interface InsertSingleWithOnConflictArgs where?: Filters } } + export interface UpdateArgs { where?: Filters set: Partial> @@ -543,27 +629,31 @@ export type FiltersCore = Partial<{ export type Filters = FiltersCore & { OR?: FiltersCore[] + AND?: FiltersCore[] + NOT?: FiltersCore } export interface ColumnFiltersCore { eq?: TType ne?: TType - lt?: TType - lte?: TType gt?: TType gte?: TType + lt?: TType + lte?: TType + in?: TType[] + notIn?: TType[] like?: TType extends string ? string : never - notLike?: TType extends string ? string : never ilike?: TType extends string ? string : never + notLike?: TType extends string ? string : never notIlike?: TType extends string ? string : never - inArray?: TType[] - notInArray?: TType[] isNull?: boolean isNotNull?: boolean } export interface ColumnFilters extends ColumnFiltersCore { OR?: ColumnFiltersCore[] + AND?: ColumnFiltersCore[] + NOT?: ColumnFiltersCore } export interface MutationResult { diff --git a/packages/drizzle/src/factory/relation-field-loader.ts b/packages/drizzle/src/factory/relation-field-loader.ts new file mode 100644 index 00000000..76ec7500 --- /dev/null +++ b/packages/drizzle/src/factory/relation-field-loader.ts @@ -0,0 +1,244 @@ +import { + type BaseField, + LoomDataLoader, + type ResolverPayload, + getMemoizationMap, + mapValue, +} from "@gqloom/core" +import { + type Column, + Many, + type Relation, + type Table, + getTableName, + inArray, +} from "drizzle-orm" +import { + getParentPath, + getPrimaryColumns, + getSelectedColumns, + inArrayMultiple, +} from "../helper" +import type { AnyQueryBuilder, BaseDatabase } from "./types" + +interface RelationFieldsLoaderInput { + id: number + selector: RelationFieldSelector + parent: any + args: any + payload: ResolverPayload | undefined +} + +export class RelationFieldsLoader extends LoomDataLoader< + [ + selector: (loader: RelationFieldsLoader) => RelationFieldSelector, + parent: any, + args: any, + payload: ResolverPayload | undefined, + ], + any +> { + public static getLoaderByPath( + payload: ResolverPayload | undefined, + ...args: ConstructorParameters + ): RelationFieldsLoader { + if (!payload) return new RelationFieldsLoader(...args) + const memoMap = getMemoizationMap(payload) + const parentPath = getParentPath(payload.info) + /** parentPath -> loader */ + const loaderMap: Map = + memoMap.get(RelationFieldsLoader) ?? new Map() + memoMap.set(RelationFieldsLoader, loaderMap) + const loader = + loaderMap.get(parentPath) ?? new RelationFieldsLoader(...args) + loaderMap.set(parentPath, loader) + + return loader + } + + protected queryBuilder: AnyQueryBuilder + protected selectors: Map + + public constructor( + protected db: BaseDatabase, + protected table: Table + ) { + const queryBuilder = matchQueryBuilder(db.query, table) + if (!queryBuilder) { + throw new Error( + `Query builder not found for source table ${getTableName(table)}` + ) + } + super() + + this.queryBuilder = queryBuilder + this.selectors = new Map() + } + + protected async batchLoad( + inputBatch: [ + selector: (loader: RelationFieldsLoader) => RelationFieldSelector, + parent: any, + args: any, + payload: ResolverPayload | undefined, + ][] + ): Promise { + const inputs = inputBatch.map( + ([selector, ...rest], index) => ({ + id: index, + parentPath: rest[2]?.info + ? getParentPath(rest[2]?.info) + : `isolated:${index}`, + selector: this.getSelector(selector), + parent: rest[0], + args: rest[1], + payload: rest[2], + }) + ) + /** field -> inputs */ + const inputGroups = new Map< + RelationFieldSelector, + RelationFieldsLoaderInput[] + >() + for (const input of inputs) { + const groupByField = inputGroups.get(input.selector) ?? [] + groupByField.push(input) + inputGroups.set(input.selector, groupByField) + } + + const results = await this.loadBatchParents(inputGroups) + + return inputs.map(({ id }) => results.get(id)) + } + + protected async loadBatchParents( + inputGroups: Map + ): Promise> { + const { parents, relations } = this.getParentRelation(inputGroups) + + const parentsWithRelationsList = await this.queryBuilder.findMany({ + where: { + RAW: (table) => whereByParent(table, Array.from(parents.values())), + }, + with: relations as never, + columns: Object.fromEntries( + getPrimaryColumns(this.table).map(([key]) => [key, true]) + ), + }) + + const parentsWithRelations = new Map( + parentsWithRelationsList.map((parent) => [ + keyForParent(this.table, parent), + parent, + ]) + ) + + /** input.id -> result */ + const results = new Map() + + for (const [selector, inputs] of inputGroups) { + for (const input of inputs) { + const parent = parentsWithRelations.get( + keyForParent(this.table, input.parent) + ) + const result = + parent?.[selector.relationName] ?? (selector.isMany ? [] : null) + results.set(input.id, result) + } + } + + return results + } + + protected getParentRelation( + inputGroups: Map + ) { + const parents = new Map() + const relations: any = {} + for (const [selector, inputs] of inputGroups) { + const selectedColumns = getSelectedColumns( + selector.targetTable, + inputs.map((input) => input.payload) + ) + const primaryColumns = Object.fromEntries( + getPrimaryColumns(selector.targetTable) + ) + const columns = { ...selectedColumns, ...primaryColumns } + relations[selector.relationName] = selector.selectField( + inputs[0].args, + columns + ) + for (const input of inputs) { + parents.set(keyForParent(this.table, input.parent), input.parent) + } + } + return { parents, relations } + } + + protected getSelector( + selector: (loader: RelationFieldsLoader) => RelationFieldSelector + ) { + const existing = this.selectors.get(selector) + if (existing) return existing + const selectorInstance = selector(this) + this.selectors.set(selector, selectorInstance) + return selectorInstance + } +} + +export class RelationFieldSelector { + public isMany: boolean + + public constructor( + public readonly relationName: string | number | symbol, + relation: Relation, + public readonly targetTable: Table + ) { + this.isMany = relation instanceof Many + } + + public selectField(args: any, columns: Partial>) { + if (this.isMany) { + return { + where: { RAW: args.where }, + orderBy: args.orderBy, + limit: args.limit, + offset: args.offset, + columns: mapValue(columns, () => true), + } + } + return { + where: { RAW: args.where }, + columns: mapValue(columns, () => true), + } + } +} + +function matchQueryBuilder( + queries: Record, + table: any +): AnyQueryBuilder | undefined { + return Object.values(queries).find((qb) => qb.table === table) +} + +function keyForParent(table: Table, parent: any) { + return getPrimaryColumns(table) + .map(([key]) => parent[key]) + .join() +} + +function whereByParent(table: Table, parents: any[]) { + const primaryColumns = getPrimaryColumns(table) + if (primaryColumns.length === 1) { + const [key, column] = primaryColumns[0] + return inArray( + column, + parents.map((parent) => parent[key]) + ) + } + return inArrayMultiple( + primaryColumns.map((it) => it[1]), + parents.map((parent) => primaryColumns.map(([key]) => parent[key])), + table + ) +} diff --git a/packages/drizzle/src/factory/resolver-mysql.ts b/packages/drizzle/src/factory/resolver-mysql.ts index 9322a9fd..a4e56f73 100644 --- a/packages/drizzle/src/factory/resolver-mysql.ts +++ b/packages/drizzle/src/factory/resolver-mysql.ts @@ -21,10 +21,14 @@ import { import { DrizzleResolverFactory } from "./resolver" import type { DeleteMutationReturningSuccess, + DeleteOptions, DrizzleResolverReturningSuccess, InsertArrayMutationReturningSuccess, + InsertArrayOptions, InsertSingleMutationReturningSuccess, + InsertSingleOptions, UpdateMutationReturningSuccess, + UpdateOptions, } from "./types" export class DrizzleMySQLResolverFactory< @@ -41,22 +45,23 @@ export class DrizzleMySQLResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertArrayMutationReturningSuccess >[] } = {}): InsertArrayMutationReturningSuccess { input ??= silk( - () => this.inputFactory.insertArrayArgs() as GraphQLOutputType - ) + () => this.inputFactory.insertArrayArgs() as GraphQLOutputType, + this.argsTransformer.toInsertArrayOptions + ) as any return new MutationFactoryWithResolve( DrizzleMySQLResolverFactory.mutationResult, { ...options, input, - resolve: async (input) => { - await this.db.insert(this.table).values(input.values) + resolve: async (args: InsertArrayOptions) => { + await this.db.insert(this.table).values(args.values) return { isSuccess: true } }, } as MutationOptions @@ -67,21 +72,22 @@ export class DrizzleMySQLResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertSingleMutationReturningSuccess >[] } = {}): InsertSingleMutationReturningSuccess { input ??= silk( - () => this.inputFactory.insertSingleArgs() as GraphQLOutputType - ) + () => this.inputFactory.insertSingleArgs() as GraphQLOutputType, + this.argsTransformer.toInsertSingleOptions + ) as any return new MutationFactoryWithResolve( DrizzleMySQLResolverFactory.mutationResult, { ...options, input, - resolve: async (args) => { + resolve: async (args: InsertSingleOptions) => { await this.db.insert(this.table).values(args.value) return { isSuccess: true } }, @@ -93,20 +99,23 @@ export class DrizzleMySQLResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware>[] } = {}): UpdateMutationReturningSuccess { - input ??= silk(() => this.inputFactory.updateArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.updateArgs() as GraphQLOutputType, + this.argsTransformer.toUpdateOptions + ) as any return new MutationFactoryWithResolve( DrizzleMySQLResolverFactory.mutationResult, { ...options, input, - resolve: async (args) => { - let query = this.db.update(this.table).set(args.set) + resolve: async (args: UpdateOptions) => { + let query: any = this.db.update(this.table).set(args.set) if (args.where) { - query = query.where(this.extractFilters(args.where)) as any + query = query.where(args.where) } await query return { isSuccess: true } @@ -119,20 +128,23 @@ export class DrizzleMySQLResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk middlewares?: Middleware>[] } = {}): DeleteMutationReturningSuccess { - input ??= silk(() => this.inputFactory.deleteArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.deleteArgs() as GraphQLOutputType, + this.argsTransformer.toDeleteOptions + ) as any return new MutationFactoryWithResolve( DrizzleMySQLResolverFactory.mutationResult, { ...options, input, - resolve: async (args) => { - let query = this.db.delete(this.table) + resolve: async (args: DeleteOptions) => { + let query: any = this.db.delete(this.table) if (args.where) { - query = query.where(this.extractFilters(args.where)) as any + query = query.where(args.where) } await query return { isSuccess: true } diff --git a/packages/drizzle/src/factory/resolver-postgres.ts b/packages/drizzle/src/factory/resolver-postgres.ts index 52572754..a211fb84 100644 --- a/packages/drizzle/src/factory/resolver-postgres.ts +++ b/packages/drizzle/src/factory/resolver-postgres.ts @@ -20,10 +20,14 @@ import type { import { DrizzleResolverFactory } from "./resolver" import type { DeleteMutationReturningItems, + DeleteOptions, DrizzleResolverReturningItems, InsertArrayMutationReturningItems, + InsertArrayWithOnConflictOptions, InsertSingleMutationReturningItem, + InsertSingleWithOnConflictOptions, UpdateMutationReturningItems, + UpdateOptions, } from "./types" export class DrizzlePostgresResolverFactory< @@ -34,38 +38,30 @@ export class DrizzlePostgresResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertArrayMutationReturningItems >[] } = {}): InsertArrayMutationReturningItems { input ??= silk( () => - this.inputFactory.insertArrayWithOnConflictArgs() as GraphQLOutputType - ) + this.inputFactory.insertArrayWithOnConflictArgs() as GraphQLOutputType, + this.argsTransformer.toInsertArrayWithOnConflictOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args: InsertArrayWithOnConflictArgs, payload) => { + resolve: async ( + args: InsertArrayWithOnConflictOptions, + payload + ) => { let query: any = this.db.insert(this.table).values(args.values) if (args.onConflictDoUpdate) { - query = query.onConflictDoUpdate({ - target: args.onConflictDoUpdate.target.map((t) => this.toColumn(t)), - set: args.onConflictDoUpdate.set, - targetWhere: this.extractFilters( - args.onConflictDoUpdate.targetWhere - ), - setWhere: this.extractFilters(args.onConflictDoUpdate.setWhere), - }) + query = query.onConflictDoUpdate(args.onConflictDoUpdate) } if (args.onConflictDoNothing) { - query = query.onConflictDoNothing({ - target: args.onConflictDoNothing.target?.map((t) => - this.toColumn(t) - ), - where: this.extractFilters(args.onConflictDoNothing.where), - }) + query = query.onConflictDoNothing(args.onConflictDoNothing) } return await query.returning(getSelectedColumns(this.table, payload)) }, @@ -78,41 +74,30 @@ export class DrizzlePostgresResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertSingleMutationReturningItem >[] } = {}): InsertSingleMutationReturningItem { input ??= silk( () => - this.inputFactory.insertSingleWithOnConflictArgs() as GraphQLOutputType - ) + this.inputFactory.insertSingleWithOnConflictArgs() as GraphQLOutputType, + this.argsTransformer.toInsertSingleWithOnConflictOptions + ) as any return new MutationFactoryWithResolve(this.output.$nullable(), { ...options, input, resolve: async ( - args: InsertSingleWithOnConflictArgs, + args: InsertSingleWithOnConflictOptions, payload ) => { let query: any = this.db.insert(this.table).values(args.value) if (args.onConflictDoUpdate) { - query = query.onConflictDoUpdate({ - target: args.onConflictDoUpdate.target.map((t) => this.toColumn(t)), - set: args.onConflictDoUpdate.set, - targetWhere: this.extractFilters( - args.onConflictDoUpdate.targetWhere - ), - setWhere: this.extractFilters(args.onConflictDoUpdate.setWhere), - }) + query = query.onConflictDoUpdate(args.onConflictDoUpdate) } if (args.onConflictDoNothing) { - query = query.onConflictDoNothing({ - target: args.onConflictDoNothing.target?.map((t) => - this.toColumn(t) - ), - where: this.extractFilters(args.onConflictDoNothing.where), - }) + query = query.onConflictDoNothing(args.onConflictDoNothing) } const result = await query.returning( getSelectedColumns(this.table, payload) @@ -126,18 +111,21 @@ export class DrizzlePostgresResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware>[] } = {}): UpdateMutationReturningItems { - input ??= silk(() => this.inputFactory.updateArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.updateArgs() as GraphQLOutputType, + this.argsTransformer.toUpdateOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args, payload) => { - const query = this.db.update(this.table).set(args.set) + resolve: async (args: UpdateOptions, payload) => { + let query: any = this.db.update(this.table).set(args.set) if (args.where) { - query.where(this.extractFilters(args.where)) + query = query.where(args.where) } return await query.returning(getSelectedColumns(this.table, payload)) }, @@ -148,18 +136,21 @@ export class DrizzlePostgresResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk middlewares?: Middleware>[] } = {}): DeleteMutationReturningItems { - input ??= silk(() => this.inputFactory.deleteArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.deleteArgs() as GraphQLOutputType, + this.argsTransformer.toDeleteOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args, payload) => { - const query = this.db.delete(this.table) + resolve: async (args: DeleteOptions, payload) => { + let query: any = this.db.delete(this.table) if (args.where) { - query.where(this.extractFilters(args.where)) + query = query.where(args.where) } return await query.returning(getSelectedColumns(this.table, payload)) }, diff --git a/packages/drizzle/src/factory/resolver-sqlite.ts b/packages/drizzle/src/factory/resolver-sqlite.ts index b8d91b0b..fb8dcb40 100644 --- a/packages/drizzle/src/factory/resolver-sqlite.ts +++ b/packages/drizzle/src/factory/resolver-sqlite.ts @@ -20,10 +20,14 @@ import type { import { DrizzleResolverFactory } from "./resolver" import type { DeleteMutationReturningItems, + DeleteOptions, DrizzleResolverReturningItems, InsertArrayMutationReturningItems, + InsertArrayWithOnConflictOptions, InsertSingleMutationReturningItem, + InsertSingleWithOnConflictOptions, UpdateMutationReturningItems, + UpdateOptions, } from "./types" export class DrizzleSQLiteResolverFactory< @@ -34,38 +38,30 @@ export class DrizzleSQLiteResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertArrayMutationReturningItems >[] } = {}): InsertArrayMutationReturningItems { input ??= silk( () => - this.inputFactory.insertArrayWithOnConflictArgs() as GraphQLOutputType - ) + this.inputFactory.insertArrayWithOnConflictArgs() as GraphQLOutputType, + this.argsTransformer.toInsertArrayWithOnConflictOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args: InsertArrayWithOnConflictArgs, payload) => { + resolve: async ( + args: InsertArrayWithOnConflictOptions, + payload + ) => { let query: any = this.db.insert(this.table).values(args.values) if (args.onConflictDoUpdate) { - query = query.onConflictDoUpdate({ - target: args.onConflictDoUpdate.target.map((t) => this.toColumn(t)), - set: args.onConflictDoUpdate.set, - targetWhere: this.extractFilters( - args.onConflictDoUpdate.targetWhere - ), - setWhere: this.extractFilters(args.onConflictDoUpdate.setWhere), - }) + query = query.onConflictDoUpdate(args.onConflictDoUpdate) } if (args.onConflictDoNothing) { - query = query.onConflictDoNothing({ - target: args.onConflictDoNothing.target?.map((t) => - this.toColumn(t) - ), - where: this.extractFilters(args.onConflictDoNothing.where), - }) + query = query.onConflictDoNothing(args.onConflictDoNothing) } return await query.returning(getSelectedColumns(this.table, payload)) }, @@ -78,40 +74,30 @@ export class DrizzleSQLiteResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware< InsertSingleMutationReturningItem >[] } = {}): InsertSingleMutationReturningItem { input ??= silk( () => - this.inputFactory.insertSingleWithOnConflictArgs() as GraphQLOutputType - ) + this.inputFactory.insertSingleWithOnConflictArgs() as GraphQLOutputType, + this.argsTransformer.toInsertSingleWithOnConflictOptions + ) as any + return new MutationFactoryWithResolve(this.output.$nullable(), { ...options, input, resolve: async ( - args: InsertSingleWithOnConflictArgs, + args: InsertSingleWithOnConflictOptions, payload ) => { let query: any = this.db.insert(this.table).values(args.value) if (args.onConflictDoUpdate) { - query = query.onConflictDoUpdate({ - target: args.onConflictDoUpdate.target.map((t) => this.toColumn(t)), - set: args.onConflictDoUpdate.set, - targetWhere: this.extractFilters( - args.onConflictDoUpdate.targetWhere - ), - setWhere: this.extractFilters(args.onConflictDoUpdate.setWhere), - }) + query = query.onConflictDoUpdate(args.onConflictDoUpdate) } if (args.onConflictDoNothing) { - query = query.onConflictDoNothing({ - target: args.onConflictDoNothing.target?.map((t) => - this.toColumn(t) - ), - where: this.extractFilters(args.onConflictDoNothing.where), - }) + query = query.onConflictDoNothing(args.onConflictDoNothing) } return ( await query.returning(getSelectedColumns(this.table, payload)) @@ -124,18 +110,21 @@ export class DrizzleSQLiteResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware>[] } = {}): UpdateMutationReturningItems { - input ??= silk(() => this.inputFactory.updateArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.updateArgs() as GraphQLOutputType, + this.argsTransformer.toUpdateOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args, payload) => { - const query = this.db.update(this.table).set(args.set) + resolve: async (args: UpdateOptions, payload) => { + let query: any = this.db.update(this.table).set(args.set) if (args.where) { - query.where(this.extractFilters(args.where)) + query = query.where(args.where) } return await query.returning(getSelectedColumns(this.table, payload)) }, @@ -146,18 +135,21 @@ export class DrizzleSQLiteResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk middlewares?: Middleware>[] } = {}): DeleteMutationReturningItems { - input ??= silk(() => this.inputFactory.deleteArgs() as GraphQLOutputType) + input ??= silk( + () => this.inputFactory.deleteArgs() as GraphQLOutputType, + this.argsTransformer.toDeleteOptions + ) as any return new MutationFactoryWithResolve(this.output.$list(), { ...options, input, - resolve: async (args, payload) => { - const query = this.db.delete(this.table) + resolve: async (args: DeleteOptions, payload) => { + let query: any = this.db.delete(this.table) if (args.where) { - query.where(this.extractFilters(args.where)) + query = query.where(args.where) } return await query.returning(getSelectedColumns(this.table, payload)) }, diff --git a/packages/drizzle/src/factory/resolver.ts b/packages/drizzle/src/factory/resolver.ts index 6ab6aa12..987753b6 100644 --- a/packages/drizzle/src/factory/resolver.ts +++ b/packages/drizzle/src/factory/resolver.ts @@ -1,5 +1,4 @@ import { - EasyDataLoader, FieldFactoryWithResolve, type FieldOptions, type GraphQLFieldOptions, @@ -9,80 +8,71 @@ import { type ObjectChainResolver, QueryFactoryWithResolve, type QueryOptions, - type ResolverPayload, capitalize, - getMemoizationMap, loom, mapValue, silk, } from "@gqloom/core" import { - type Column, type InferSelectModel, Many, type Relation, - type SQL, - type Table, - and, - asc, - desc, - eq, + Table, + type TableRelationalConfig, + type TablesRelationalConfig, getTableColumns, getTableName, - gt, - gte, - ilike, - inArray, - isNotNull, - isNull, - like, - lt, - lte, - ne, - normalizeRelation, - notIlike, - notInArray, - notLike, - or, } from "drizzle-orm" -import { GraphQLError, GraphQLInt, GraphQLNonNull } from "graphql" +import { GraphQLInt, GraphQLNonNull } from "graphql" import { type DrizzleResolverFactoryOptions, DrizzleWeaver, type SelectiveTable, type TableSilk, } from ".." -import { getSelectedColumns, inArrayMultiple } from "../helper" +import { getSelectedColumns } from "../helper" import { - type ColumnFilters, type CountArgs, type DeleteArgs, DrizzleInputFactory, - type FiltersCore, type InsertArrayArgs, type InsertSingleArgs, + type RelationArgs, + type RelationToManyArgs, + type RelationToOneArgs, type SelectArrayArgs, type SelectSingleArgs, type UpdateArgs, } from "./input" +import { + RelationFieldSelector, + RelationFieldsLoader, +} from "./relation-field-loader" +import { DrizzleArgsTransformer } from "./transform" import type { BaseDatabase, + CountOptions, CountQuery, DeleteMutation, + DeleteOptions, DrizzleQueriesResolver, - InferRelationTable, - InferSelectArrayOptions, - InferSelectSingleOptions, InferTableName, InferTableRelationalConfig, InsertArrayMutation, + InsertArrayOptions, InsertSingleMutation, + InsertSingleOptions, QueryBuilder, - RelationManyField, - RelationOneField, + QueryFieldOptions, + QueryToManyFieldOptions, + QueryToOneFieldOptions, + RelationField, + SelectArrayOptions, SelectArrayQuery, + SelectSingleOptions, SelectSingleQuery, UpdateMutation, + UpdateOptions, } from "./types" export abstract class DrizzleResolverFactory< @@ -91,6 +81,7 @@ export abstract class DrizzleResolverFactory< > { protected readonly inputFactory: DrizzleInputFactory protected readonly tableName: InferTableName + protected readonly argsTransformer: DrizzleArgsTransformer public constructor( protected readonly db: TDatabase, protected readonly table: TTable, @@ -98,6 +89,7 @@ export abstract class DrizzleResolverFactory< ) { this.inputFactory = new DrizzleInputFactory(table, options) this.tableName = getTableName(table) + this.argsTransformer = new DrizzleArgsTransformer(table, options) } private _output?: TableSilk @@ -110,35 +102,25 @@ export abstract class DrizzleResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> - middlewares?: Middleware>[] - } = {}): SelectArrayQuery { - input ??= silk< - InferSelectArrayOptions, - SelectArrayArgs - >( + input?: GraphQLSilk + middlewares?: Middleware>[] + } = {}): SelectArrayQuery { + input ??= silk>( () => this.inputFactory.selectArrayArgs(), - (args) => ({ - value: { - where: this.extractFilters(args.where), - orderBy: this.extractOrderBy(args.orderBy), - limit: args.limit, - offset: args.offset, - }, - }) - ) as GraphQLSilk, TInputI> + this.argsTransformer.toSelectArrayOptions + ) as GraphQLSilk return new QueryFactoryWithResolve(this.output.$list(), { input, ...options, - resolve: (opts, payload) => { + resolve: (opts: SelectArrayOptions | undefined, payload) => { let query: any = (this.db as any) .select(getSelectedColumns(this.table, payload)) .from(this.table) - if (opts.where) query = query.where(opts.where) - if (opts.orderBy?.length) query = query.orderBy(...opts.orderBy) - if (opts.limit) query = query.limit(opts.limit) - if (opts.offset) query = query.offset(opts.offset) + if (opts?.where) query = query.where(opts.where) + if (opts?.orderBy?.length) query = query.orderBy(...opts.orderBy) + if (opts?.limit) query = query.limit(opts.limit) + if (opts?.offset) query = query.offset(opts.offset) return query }, } as QueryOptions) @@ -148,34 +130,25 @@ export abstract class DrizzleResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> - middlewares?: Middleware>[] - } = {}): SelectSingleQuery { - input ??= silk< - InferSelectSingleOptions, - SelectSingleArgs - >( + input?: GraphQLSilk + middlewares?: Middleware>[] + } = {}): SelectSingleQuery { + input ??= silk>( () => this.inputFactory.selectSingleArgs(), - (args) => ({ - value: { - where: this.extractFilters(args.where), - orderBy: this.extractOrderBy(args.orderBy), - offset: args.offset, - }, - }) - ) as GraphQLSilk, TInputI> + this.argsTransformer.toSelectSingleOptions + ) as GraphQLSilk return new QueryFactoryWithResolve(this.output.$nullable(), { input, ...options, - resolve: (opts, payload) => { + resolve: (opts: SelectSingleOptions | undefined, payload) => { let query: any = (this.db as any) .select(getSelectedColumns(this.table, payload)) .from(this.table) - if (opts.where) query = query.where(opts.where) - if (opts.orderBy?.length) query = query.orderBy(...opts.orderBy) + if (opts?.where) query = query.where(opts.where) + if (opts?.orderBy?.length) query = query.orderBy(...opts.orderBy) query = query.limit(1) - if (opts.offset) query = query.offset(opts.offset) + if (opts?.offset) query = query.offset(opts.offset) return query.then((res: any) => res[0]) }, } as QueryOptions) @@ -185,273 +158,107 @@ export abstract class DrizzleResolverFactory< input, ...options }: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk middlewares?: Middleware>[] } = {}): CountQuery { - input ??= silk, CountArgs>(() => - this.inputFactory.countArgs() - ) as GraphQLSilk, TInputI> + input ??= silk>( + () => this.inputFactory.countArgs(), + this.argsTransformer.toCountOptions + ) as GraphQLSilk return new QueryFactoryWithResolve(silk(new GraphQLNonNull(GraphQLInt)), { input, ...options, - resolve: (args) => { - return this.db.$count(this.table, this.extractFilters(args.where)) + resolve: (args: CountOptions) => { + return this.db.$count(this.table, args.where) }, } as QueryOptions) } - protected extractOrderBy( - orders?: SelectArrayArgs["orderBy"] - ): SQL[] | undefined { - if (orders == null) return - const answer: SQL[] = [] - const columns = getTableColumns(this.table) - for (const order of orders) { - for (const [column, direction] of Object.entries(order)) { - if (!direction) continue - if (column in columns) { - answer.push( - direction === "asc" ? asc(columns[column]) : desc(columns[column]) - ) - } - } - } - return answer - } - - protected extractFilters( - filters: SelectArrayArgs["where"] - ): SQL | undefined { - if (filters == null) return - const tableName = getTableName(this.table) - - if (!filters.OR?.length) delete filters.OR - - const entries = Object.entries(filters as FiltersCore) - - if (filters.OR) { - if (entries.length > 1) { - throw new GraphQLError( - `WHERE ${tableName}: Cannot specify both fields and 'OR' in table filters!` - ) - } - - const variants = [] as SQL[] - - for (const variant of filters.OR) { - const extracted = this.extractFilters(variant) - if (extracted) variants.push(extracted) - } - - return or(...variants) - } - - const variants: SQL[] = [] - for (const [columnName, operators] of entries) { - if (operators == null) continue - - const column = getTableColumns(this.table)[columnName]! - variants.push(this.extractFiltersColumn(column, columnName, operators)!) - } - - return and(...variants) - } - - protected extractFiltersColumn( - column: TColumn, - columnName: string, - operators: ColumnFilters - ): SQL | undefined { - if (!operators.OR?.length) delete operators.OR - - const entries = Object.entries(operators) - - if (operators.OR) { - if (entries.length > 1) { - throw new GraphQLError( - `WHERE ${columnName}: Cannot specify both fields and 'OR' in column operators!` - ) - } - - const variants = [] as SQL[] - - for (const variant of operators.OR) { - const extracted = this.extractFiltersColumn(column, columnName, variant) - - if (extracted) variants.push(extracted) - } - - return or(...variants) - } - - const variants: SQL[] = [] - const binaryOperators = { eq, ne, gt, gte, lt, lte } - const textOperators = { like, notLike, ilike, notIlike } - const arrayOperators = { inArray, notInArray } - const nullOperators = { isNull, isNotNull } - - for (const [operatorName, operatorValue] of entries) { - if (operatorValue === null || operatorValue === false) continue - - if (operatorName in binaryOperators) { - const operator = - binaryOperators[operatorName as keyof typeof binaryOperators] - variants.push(operator(column, operatorValue)) - } else if (operatorName in textOperators) { - const operator = - textOperators[operatorName as keyof typeof textOperators] - variants.push(operator(column, operatorValue)) - } else if (operatorName in arrayOperators) { - const operator = - arrayOperators[operatorName as keyof typeof arrayOperators] - variants.push(operator(column, operatorValue)) - } else if (operatorName in nullOperators) { - const operator = - nullOperators[operatorName as keyof typeof nullOperators] - if (operatorValue === true) variants.push(operator(column)) - } - } - - return and(...variants) - } - - protected toColumn(columnName: string) { - const column = getTableColumns(this.table)[columnName] - if (!column) { - throw new Error( - `Column ${columnName} not found in table ${this.tableName}` - ) - } - return column - } - public relationField< TRelationName extends keyof InferTableRelationalConfig< - QueryBuilder> + QueryBuilder >["relations"], + TInputI = RelationArgs, >( relationName: TRelationName, - options?: GraphQLFieldOptions & { + { + input, + ...options + }: GraphQLFieldOptions & { + input?: GraphQLSilk< + QueryFieldOptions, + TInputI + > middlewares?: Middleware< - InferTableRelationalConfig< - QueryBuilder> - >["relations"][TRelationName] extends Many - ? RelationManyField< - TTable, - InferRelationTable - > - : RelationOneField< - TTable, - InferRelationTable - > + RelationField >[] - } - ): InferTableRelationalConfig< - QueryBuilder> - >["relations"][TRelationName] extends Many - ? RelationManyField< - TTable, - InferRelationTable - > - : RelationOneField< - TTable, - InferRelationTable - > { - const relation = this.db._.schema?.[this.tableName]?.relations?.[ - relationName - ] as Relation - if (!relation) { + } = {} + ): RelationField { + const [relation, targetTable] = (() => { + const tableKey = matchTableByTablesConfig( + this.db._.relations.tablesConfig, + this.table + )?.tsName + if (!tableKey) return [undefined, undefined] + const relation = this.db._.relations["config"]?.[tableKey]?.[ + relationName + ] as Relation + const targetTable = relation?.targetTable + return [relation, targetTable] + })() + if (!relation || !(targetTable instanceof Table)) { throw new Error( - `GQLoom-Drizzle Error: Relation ${this.tableName}.${String(relationName)} not found in drizzle instance. Did you forget to pass relations to drizzle constructor?` + `GQLoom-Drizzle Error: Relation ${this.tableName}.${String( + relationName + )} not found in drizzle instance. Did you forget to pass relations to drizzle constructor?` ) } - const output = DrizzleWeaver.unravel(relation.referencedTable) - const table = relation.referencedTable - - const normalizedRelation = normalizeRelation( - this.db._.schema, - this.db._.tableNamesMap, - relation + const output = DrizzleWeaver.unravel(targetTable) + const toMany = relation instanceof Many + const targetInputFactory = new DrizzleInputFactory(targetTable) + + input ??= ( + toMany + ? silk, RelationToManyArgs>( + () => targetInputFactory.relationToManyArgs(), + this.argsTransformer.toQueryToManyFieldOptions + ) + : silk, RelationToOneArgs>( + () => targetInputFactory.relationToOneArgs(), + this.argsTransformer.toQueryToOneFieldOptions + ) + ) as GraphQLSilk< + QueryFieldOptions, + TInputI + > + + /** columnTableName -> columnTsKey */ + const columnKeys = new Map( + Object.entries(getTableColumns(targetTable)).map(([name, col]) => [ + col.name, + name, + ]) ) - const isList = relation instanceof Many - const fieldsLength = normalizedRelation.fields.length - - const getKeyByField = (parent: any) => { - if (fieldsLength === 1) { - return parent[normalizedRelation.fields[0].name] - } - return normalizedRelation.fields - .map((field) => parent[field.name]) - .join("-") - } - - const referenceColumns = Object.fromEntries( - normalizedRelation.references.map((col) => [col.name, col]) + const dependencies = relation.sourceColumns.map( + (col) => columnKeys.get(col.name) ?? col.name ) - - const getKeyByReference = (item: any) => { - if (fieldsLength === 1) { - return item[normalizedRelation.references[0].name] - } - return normalizedRelation.references - .map((reference) => item[reference.name]) - .join("-") - } - - const initLoader = () => { - return new EasyDataLoader( - async (inputs: [any, payload: ResolverPayload | undefined][]) => { - const where = (() => { - if (fieldsLength === 1) { - const values = inputs.map( - (input) => input[0][normalizedRelation.fields[0].name] - ) - return inArray(normalizedRelation.references[0], values) - } - const values = inputs.map((input) => - normalizedRelation.fields.map((field) => input[0][field.name]) - ) - return inArrayMultiple(normalizedRelation.references, values) - })() - const selectedColumns = getSelectedColumns( - table, - inputs.map((input) => input[1]) - ) - - const list = await (this.db as any) - .select({ ...selectedColumns, ...referenceColumns }) - .from(table) - .where(where) - - const groups = new Map() - for (const item of list) { - const key = getKeyByReference(item) - isList - ? groups.set(key, [...(groups.get(key) ?? []), item]) - : groups.set(key, item) - } - return inputs.map(([parent]) => { - const key = getKeyByField(parent) - return groups.get(key) ?? (isList ? [] : null) - }) - } - ) - } + const initSelector = () => + new RelationFieldSelector(relationName, relation, targetTable) return new FieldFactoryWithResolve( - isList ? output.$list() : output.$nullable(), + toMany ? output.$list() : output.$nullable(), { + input, ...options, - dependencies: ["tableName"], - resolve: (parent, _input, payload) => { - const loader = (() => { - if (!payload) return initLoader() - const memoMap = getMemoizationMap(payload) - if (!memoMap.has(initLoader)) memoMap.set(initLoader, initLoader()) - return memoMap.get(initLoader) as ReturnType - })() - return loader.load([parent, payload]) + dependencies, + resolve: (parent, input, payload) => { + const loader = RelationFieldsLoader.getLoaderByPath( + payload, + this.db, + this.table + ) + return loader.load([initSelector, parent, input, payload]) }, } as FieldOptions ) @@ -467,7 +274,7 @@ export abstract class DrizzleResolverFactory< const name = options?.name ?? this.tableName const fields: Record> = mapValue( - this.db._.schema?.[this.tableName]?.relations ?? {}, + this.db._.relations.config[this.tableName] ?? {}, (_, key) => this.relationField(key) ) @@ -494,7 +301,7 @@ export abstract class DrizzleResolverFactory< middlewares?: Middleware[] }): ObjectChainResolver< GraphQLSilk, SelectiveTable>, - DrizzleQueriesResolver + DrizzleQueriesResolver > { const name = options?.name ?? this.tableName @@ -517,29 +324,40 @@ export abstract class DrizzleResolverFactory< public abstract insertArrayMutation>( options?: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware[] } ): InsertArrayMutation public abstract insertSingleMutation>( options?: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware[] } ): InsertSingleMutation public abstract updateMutation>( options?: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk, TInputI> middlewares?: Middleware[] } ): UpdateMutation public abstract deleteMutation>( options?: GraphQLFieldOptions & { - input?: GraphQLSilk, TInputI> + input?: GraphQLSilk middlewares?: Middleware[] } ): DeleteMutation } + +function matchTableByTablesConfig( + tablesConfig: TablesRelationalConfig, + targetTable: Table +): TableRelationalConfig | undefined { + for (const config of Object.values(tablesConfig)) { + if (config.table === targetTable) { + return config + } + } +} diff --git a/packages/drizzle/src/factory/transform.ts b/packages/drizzle/src/factory/transform.ts new file mode 100644 index 00000000..0818c10e --- /dev/null +++ b/packages/drizzle/src/factory/transform.ts @@ -0,0 +1,508 @@ +import type { MayPromise, StandardSchemaV1 } from "@gqloom/core" +import type { GraphQLSilk } from "@gqloom/core" +import { + type Column, + type SQL, + type Table, + and, + asc, + desc, + eq, + getTableColumns, + getTableName, + gt, + gte, + ilike, + inArray, + isNotNull, + isNull, + like, + lt, + lte, + ne, + not, + notIlike, + notInArray, + notLike, + or, +} from "drizzle-orm" +import type { DrizzleResolverFactoryOptions } from "../types" +import type { + ColumnFilters, + CountArgs, + DeleteArgs, + Filters, + FiltersCore, + InsertArrayArgs, + InsertArrayWithOnConflictArgs, + InsertSingleArgs, + InsertSingleWithOnConflictArgs, + RelationToManyArgs, + RelationToOneArgs, + SelectArrayArgs, + SelectSingleArgs, + UpdateArgs, +} from "./input" +import type { + CountOptions, + DeleteOptions, + InsertArrayOptions, + InsertArrayWithOnConflictOptions, + InsertSingleOptions, + InsertSingleWithOnConflictOptions, + QueryToManyFieldOptions, + QueryToOneFieldOptions, + SelectArrayOptions, + SelectSingleOptions, + UpdateOptions, +} from "./types" + +export class DrizzleArgsTransformer { + public toSelectArrayOptions: ( + args: SelectArrayArgs + ) => MayPromise> + + public toSelectSingleOptions: ( + args: SelectSingleArgs + ) => MayPromise> + + public toCountOptions: ( + args: CountArgs + ) => MayPromise> + + public toQueryToManyFieldOptions: ( + args: RelationToManyArgs + ) => MayPromise>> + + public toQueryToOneFieldOptions: ( + args: RelationToOneArgs + ) => MayPromise>> + + public toInsertArrayOptions: ( + args: InsertArrayArgs + ) => MayPromise>> + + public toInsertArrayWithOnConflictOptions: ( + args: InsertArrayWithOnConflictArgs + ) => MayPromise< + StandardSchemaV1.Result> + > + + public toInsertSingleOptions: ( + args: InsertSingleArgs + ) => MayPromise>> + + public toInsertSingleWithOnConflictOptions: ( + args: InsertSingleWithOnConflictArgs + ) => MayPromise< + StandardSchemaV1.Result> + > + + public toUpdateOptions: ( + args: UpdateArgs + ) => MayPromise>> + + public toDeleteOptions: ( + args: DeleteArgs + ) => MayPromise> + + public constructor( + protected readonly table: TTable, + protected readonly options: + | DrizzleResolverFactoryOptions + | undefined + ) { + this.toSelectArrayOptions = (args) => ({ + value: { + where: this.extractFilters(args.where), + orderBy: this.extractOrderBy(args.orderBy), + limit: args.limit, + offset: args.offset, + }, + }) + + this.toSelectSingleOptions = (args) => ({ + value: { + where: this.extractFilters(args.where), + orderBy: this.extractOrderBy(args.orderBy), + offset: args.offset, + }, + }) + + this.toCountOptions = (args) => ({ + value: { where: this.extractFilters(args.where) }, + }) + + this.toQueryToManyFieldOptions = (args) => ({ + value: { + where: this.extractFilters(args.where), + orderBy: args.orderBy, + limit: args.limit, + offset: args.offset, + }, + }) + + this.toQueryToOneFieldOptions = (args) => ({ + value: { + where: this.extractFilters(args.where), + }, + }) + + this.toInsertArrayOptions = async (args) => { + const valuesResult = await this.validateInsertValues(args.values, [ + "values", + ]) + + if (valuesResult.issues) return { issues: valuesResult.issues } + return { value: { values: valuesResult.value } } + } + + this.toInsertArrayWithOnConflictOptions = async (args) => { + const valuesResult = await this.validateInsertValues(args.values, [ + "values", + ]) + if (valuesResult.issues) return { issues: valuesResult.issues } + return { + value: { + values: valuesResult.value, + onConflictDoNothing: args.onConflictDoNothing + ? { + target: args.onConflictDoNothing.target?.map((t) => + this.toColumn(t) + ), + where: this.extractFilters(args.onConflictDoNothing?.where), + } + : undefined, + onConflictDoUpdate: args.onConflictDoUpdate + ? { + target: args.onConflictDoUpdate.target.map((t) => + this.toColumn(t) + ), + set: args.onConflictDoUpdate.set, + targetWhere: this.extractFilters( + args.onConflictDoUpdate?.targetWhere + ), + setWhere: this.extractFilters( + args.onConflictDoUpdate?.setWhere + ), + } + : undefined, + }, + } + } + + this.toInsertSingleOptions = async (args) => { + const valueResult = await this.validateInsertValue(args.value, ["value"]) + if (valueResult.issues) return { issues: valueResult.issues } + return { value: { value: valueResult.value } } + } + + this.toInsertSingleWithOnConflictOptions = async (args) => { + const valueResult = await this.validateInsertValue(args.value, ["value"]) + if (valueResult.issues) return { issues: valueResult.issues } + return { + value: { + value: valueResult.value, + onConflictDoNothing: args.onConflictDoNothing + ? { + target: args.onConflictDoNothing.target?.map((t) => + this.toColumn(t) + ), + where: this.extractFilters(args.onConflictDoNothing?.where), + } + : undefined, + onConflictDoUpdate: args.onConflictDoUpdate + ? { + target: args.onConflictDoUpdate.target.map((t) => + this.toColumn(t) + ), + set: args.onConflictDoUpdate.set, + targetWhere: this.extractFilters( + args.onConflictDoUpdate?.targetWhere + ), + setWhere: this.extractFilters( + args.onConflictDoUpdate?.setWhere + ), + } + : undefined, + }, + } + } + + this.toUpdateOptions = async (args) => { + const setResult = await this.validateUpdateValue(args.set, ["set"]) + if (setResult.issues) return { issues: setResult.issues } + return { + value: { + where: this.extractFilters(args.where), + set: setResult.value, + }, + } + } + + this.toDeleteOptions = (args) => ({ + value: { where: this.extractFilters(args.where) }, + }) + } + + protected async validateInsertValues( + values: TTable["$inferInsert"][], + path: ReadonlyArray + ): Promise> { + const results: TTable["$inferInsert"][] = [] + const issues: StandardSchemaV1.Issue[] = [] + + await Promise.all( + values.map(async (value, index) => { + const res = await this.validateInsertValue(value, [...path, index]) + if (res.issues) { + issues.push(...res.issues) + return + } + results[index] = res.value + }) + ) + return { value: results, ...(issues.length > 0 && { issues }) } + } + + protected async validateInsertValue( + value: TTable["$inferInsert"], + path: ReadonlyArray + ): Promise> { + const result: Record = {} + const issues: StandardSchemaV1.Issue[] = [] + for (const key of Object.keys(getTableColumns(this.table))) { + const columnSilk = this.getColumnSilk(key, "insert") + if (columnSilk == null) { + result[key] = value[key] + continue + } + const res = await columnSilk["~standard"].validate(value[key]) + if ("value" in res) { + result[key] = res.value + } + if (res.issues) { + issues.push( + ...res.issues.map((issue) => ({ + ...issue, + path: [...path, key, ...(issue.path ?? [])], + })) + ) + } + } + return { value: result, ...(issues.length > 0 && { issues }) } + } + + protected async validateUpdateValue( + value: Partial, + path: ReadonlyArray + ): Promise>> { + const result: Record = {} + const issues: StandardSchemaV1.Issue[] = [] + for (const key of Object.keys(value)) { + const columnSilk = this.getColumnSilk(key, "update") + if (columnSilk == null) { + result[key] = value[key] + continue + } + const res = await columnSilk["~standard"].validate(value[key]) + if ("value" in res) { + result[key] = res.value + } + if (res.issues) { + issues.push( + ...res.issues.map((issue) => ({ + ...issue, + path: [...path, key, ...(issue.path ?? [])], + })) + ) + } + } + return { value: result, ...(issues.length > 0 && { issues }) } + } + + protected toColumn(columnName: string) { + const column = getTableColumns(this.table)[columnName] + if (!column) { + throw new Error( + `Column ${columnName} not found in table ${getTableName(this.table)}` + ) + } + return column + } + + protected getColumnSilk( + columnName: string, + mutation: "insert" | "update" + ): GraphQLSilk | undefined { + const behavior = this.options?.input?.[columnName] + if (behavior == null || typeof behavior === "boolean") return undefined + if ("~standard" in behavior) return behavior + const mutationBehavior = behavior[mutation] + if (mutationBehavior == null || typeof mutationBehavior === "boolean") + return undefined + return mutationBehavior + } + + public extractFilters( + filters: Filters | undefined, + table?: any + ): SQL | undefined { + if (filters == null) return + table ??= this.table + + const entries = Object.entries(filters as FiltersCore) + const variants: (SQL | undefined)[] = [] + + for (const [columnName, operators] of entries) { + if (operators == null) continue + + if (columnName === "OR" && Array.isArray(operators)) { + const orConditions: SQL[] = [] + for (const variant of operators) { + const extracted = this.extractFilters(variant, table) + if (extracted) orConditions.push(extracted) + } + if (orConditions.length > 0) { + variants.push(or(...orConditions)) + } + continue + } + + if (columnName === "AND" && Array.isArray(operators)) { + const andConditions: SQL[] = [] + for (const variant of operators) { + const extracted = this.extractFilters(variant, table) + if (extracted) andConditions.push(extracted) + } + if (andConditions.length > 0) { + variants.push(and(...andConditions)) + } + continue + } + + if (columnName === "NOT" && operators) { + const extracted = this.extractFilters(operators, table) + if (extracted) { + variants.push(not(extracted)) + } + continue + } + + const column = getTableColumns(table)[columnName]! + const extractedColumn = this.extractFiltersColumn( + column, + columnName, + operators, + table + ) + if (extractedColumn) variants.push(extractedColumn) + } + + return and(...variants) + } + + public extractFiltersColumn( + column: TColumn, + columnName: string, + operators: ColumnFilters, + table?: any + ): SQL | undefined { + const entries = Object.entries(operators) + + const variants: (SQL | undefined)[] = [] + const binaryOperators = { eq, ne, gt, gte, lt, lte } + const textOperators = { like, notLike, ilike, notIlike } + const arrayOperators = { in: inArray, notIn: notInArray } + const nullOperators = { isNull, isNotNull } + + const tableColumn = table ? table[columnName] : column + + if (operators.OR) { + const orVariants = [] as SQL[] + + for (const variant of operators.OR) { + const extracted = this.extractFiltersColumn( + column, + columnName, + variant, + table + ) + + if (extracted) orVariants.push(extracted) + } + + variants.push(or(...orVariants)) + } + + if (operators.AND) { + const andVariants = [] as SQL[] + + for (const variant of operators.AND) { + const extracted = this.extractFiltersColumn( + column, + columnName, + variant, + table + ) + + if (extracted) andVariants.push(extracted) + } + + variants.push(and(...andVariants)) + } + + if (operators.NOT) { + const extracted = this.extractFiltersColumn( + column, + columnName, + operators.NOT, + table + ) + if (extracted) { + variants.push(not(extracted)) + } + } + + for (const [operatorName, operatorValue] of entries) { + if (operatorValue === null || operatorValue === false) continue + + if (operatorName in binaryOperators) { + const operator = + binaryOperators[operatorName as keyof typeof binaryOperators] + variants.push(operator(tableColumn, operatorValue)) + } else if (operatorName in textOperators) { + const operator = + textOperators[operatorName as keyof typeof textOperators] + variants.push(operator(tableColumn, operatorValue)) + } else if (operatorName in arrayOperators) { + const operator = + arrayOperators[operatorName as keyof typeof arrayOperators] + variants.push(operator(tableColumn, operatorValue)) + } else if (operatorName in nullOperators) { + const operator = + nullOperators[operatorName as keyof typeof nullOperators] + if (operatorValue === true) variants.push(operator(tableColumn)) + } + } + + return and(...variants) + } + + protected extractOrderBy( + orders?: SelectArrayArgs["orderBy"] + ): SQL[] | undefined { + if (orders == null) return + const answer: SQL[] = [] + const columns = getTableColumns(this.table) + for (const [column, direction] of Object.entries(orders)) { + if (!direction) continue + if (column in columns) { + answer.push( + direction === "asc" ? asc(columns[column]) : desc(columns[column]) + ) + } + } + return answer + } +} diff --git a/packages/drizzle/src/factory/types.ts b/packages/drizzle/src/factory/types.ts index 38a57e00..f7a483c5 100644 --- a/packages/drizzle/src/factory/types.ts +++ b/packages/drizzle/src/factory/types.ts @@ -4,7 +4,15 @@ import type { MutationFactoryWithResolve, QueryFactoryWithResolve, } from "@gqloom/core" -import type { InferSelectModel, Many, Table } from "drizzle-orm" +import type { + AnyRelations, + Column, + InferInsertModel, + InferSelectModel, + Many, + SQL, + Table, +} from "drizzle-orm" import type { MySqlDatabase } from "drizzle-orm/mysql-core" import type { RelationalQueryBuilder as MySqlRelationalQueryBuilder } from "drizzle-orm/mysql-core/query-builders/query" import type { PgDatabase } from "drizzle-orm/pg-core" @@ -20,6 +28,8 @@ import type { InsertSingleArgs, InsertSingleWithOnConflictArgs, MutationResult, + RelationToManyArgs, + RelationToOneArgs, SelectArrayArgs, SelectSingleArgs, UpdateArgs, @@ -34,13 +44,12 @@ export type DrizzleResolver< | DrizzleResolverReturningSuccess export type DrizzleQueriesResolver< - TDatabase extends BaseDatabase, TTable extends Table, TTableName extends string = TTable["_"]["name"], > = { - [key in TTableName]: SelectArrayQuery + [key in TTableName]: SelectArrayQuery } & { - [key in `${TTableName}Single`]: SelectArrayQuery + [key in `${TTableName}Single`]: SelectSingleQuery } & { [key in `${TTableName}Count`]: CountQuery } @@ -49,7 +58,7 @@ export type DrizzleResolverReturningItems< TDatabase extends BaseDatabase, TTable extends Table, TTableName extends string = TTable["_"]["name"], -> = DrizzleQueriesResolver & { +> = DrizzleQueriesResolver & { [key in `insertInto${Capitalize}`]: InsertArrayMutationReturningItems } & { [key in `insertInto${Capitalize}Single`]: InsertSingleMutationReturningItem @@ -63,7 +72,7 @@ export type DrizzleResolverReturningSuccess< TDatabase extends BaseDatabase, TTable extends Table, TTableName extends string = TTable["_"]["name"], -> = DrizzleQueriesResolver & { +> = DrizzleQueriesResolver & { [key in `insertInto${Capitalize}`]: InsertArrayMutationReturningSuccess } & { [key in `insertInto${Capitalize}Single`]: InsertSingleMutationReturningSuccess @@ -78,10 +87,10 @@ export type DrizzleResolverRelations< TTable extends Table, > = { [TRelationName in keyof InferTableRelationalConfig< - QueryBuilder> + QueryBuilder >["relations"]]: InferTableRelationalConfig< - QueryBuilder> - >["relations"][TRelationName] extends Many + QueryBuilder + >["relations"][TRelationName] extends Many ? RelationManyField< TTable, InferRelationTable @@ -93,69 +102,121 @@ export type DrizzleResolverRelations< } export interface SelectArrayQuery< - TDatabase extends BaseDatabase, TTable extends Table, TInputI = SelectArrayArgs, > extends QueryFactoryWithResolve< - InferSelectArrayOptions, + SelectArrayOptions | undefined, GraphQLSilk[], InferSelectModel[]>, - GraphQLSilk, TInputI> + GraphQLSilk > {} -export type InferSelectArrayOptions< - TDatabase extends BaseDatabase, - TTable extends Table, -> = Parameters["findMany"]>[0] +export interface SelectArrayOptions { + where?: SQL + orderBy?: (Column | SQL | SQL.Aliased)[] + limit?: number + offset?: number +} -export interface CountQuery< - TTable extends Table, - TInputI = SelectArrayArgs, -> extends QueryFactoryWithResolve< - CountArgs, +export interface CountQuery> + extends QueryFactoryWithResolve< + CountOptions, GraphQLSilk, - GraphQLSilk, TInputI> + GraphQLSilk > {} +export interface CountOptions { + where?: SQL +} + export interface SelectSingleQuery< - TDatabase extends BaseDatabase, TTable extends Table, TInputI = SelectSingleArgs, > extends QueryFactoryWithResolve< - InferSelectSingleOptions, + SelectSingleOptions | undefined, GraphQLSilk< InferSelectModel | null | undefined, InferSelectModel | null | undefined >, - GraphQLSilk, TInputI> + GraphQLSilk > {} -export type InferSelectSingleOptions< +export interface SelectSingleOptions { + where?: SQL + orderBy?: (Column | SQL | SQL.Aliased)[] + offset?: number +} + +export type RelationField< TDatabase extends BaseDatabase, TTable extends Table, -> = Parameters["findFirst"]>[0] + TRelationName extends keyof InferTableRelationalConfig< + QueryBuilder + >["relations"], +> = InferTableRelationalConfig< + QueryBuilder +>["relations"][TRelationName] extends Many + ? RelationManyField< + TTable, + InferRelationTable + > + : RelationOneField< + TTable, + InferRelationTable + > export interface RelationManyField< TTable extends Table, TRelationTable extends Table, + TInputI = RelationToManyArgs, > extends FieldFactoryWithResolve< GraphQLSilk, SelectiveTable>, GraphQLSilk< InferSelectModel[], InferSelectModel[] - > + >, + QueryToManyFieldOptions, + GraphQLSilk, TInputI> > {} +export interface QueryToManyFieldOptions { + where?: SQL | ((table: TTable) => SQL | undefined) + orderBy?: Partial> + limit?: number + offset?: number +} + export interface RelationOneField< TTable extends Table, TRelationTable extends Table, + TInputI = RelationToOneArgs, > extends FieldFactoryWithResolve< GraphQLSilk, SelectiveTable>, GraphQLSilk< InferSelectModel | null | undefined, InferSelectModel | null | undefined - > + >, + QueryToOneFieldOptions, + GraphQLSilk, TInputI> > {} +export interface QueryToOneFieldOptions { + where?: SQL | ((table: TTable) => SQL | undefined) +} + +export type QueryFieldOptions< + TDatabase extends BaseDatabase, + TTable extends Table, + TRelationName extends keyof InferTableRelationalConfig< + QueryBuilder + >["relations"], +> = InferTableRelationalConfig< + QueryBuilder +>["relations"][TRelationName] extends Many + ? QueryToManyFieldOptions< + InferRelationTable + > + : QueryToOneFieldOptions> + export type InsertArrayMutation< TTable extends Table, TInputI = InsertArrayArgs, @@ -167,18 +228,18 @@ export interface InsertArrayMutationReturningItems< TTable extends Table, TInputI = InsertArrayWithOnConflictArgs, > extends MutationFactoryWithResolve< - InsertArrayWithOnConflictArgs, + InsertArrayWithOnConflictOptions, GraphQLSilk[], InferSelectModel[]>, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} export interface InsertArrayMutationReturningSuccess< TTable extends Table, TInputI = InsertArrayArgs, > extends MutationFactoryWithResolve< - InsertArrayArgs, + InsertArrayOptions, GraphQLSilk, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} export type InsertSingleMutation< @@ -192,23 +253,52 @@ export interface InsertSingleMutationReturningItem< TTable extends Table, TInputI = InsertSingleWithOnConflictArgs, > extends MutationFactoryWithResolve< - InsertSingleWithOnConflictArgs, + InsertSingleWithOnConflictOptions, GraphQLSilk< InferSelectModel | null | undefined, InferSelectModel | null | undefined >, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} export interface InsertSingleMutationReturningSuccess< TTable extends Table, TInputI = InsertSingleArgs, > extends MutationFactoryWithResolve< - InsertSingleArgs, + InsertSingleOptions, GraphQLSilk, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} +export interface InsertArrayOptions { + values: InferInsertModel[] +} + +export interface InsertArrayWithOnConflictOptions + extends InsertArrayOptions, + InsertOnConflictInputOptions {} + +export interface InsertSingleOptions { + value: InferInsertModel +} + +export interface InsertSingleWithOnConflictOptions + extends InsertSingleOptions, + InsertOnConflictInputOptions {} + +export interface InsertOnConflictInputOptions { + onConflictDoUpdate?: { + target: Column[] + set?: Partial> + targetWhere?: SQL + setWhere?: SQL + } + onConflictDoNothing?: { + target?: Column[] + where?: SQL + } +} + export type UpdateMutation> = | UpdateMutationReturningItems | UpdateMutationReturningSuccess @@ -217,20 +307,25 @@ export interface UpdateMutationReturningItems< TTable extends Table, TInputI = UpdateArgs, > extends MutationFactoryWithResolve< - UpdateArgs, + UpdateOptions, GraphQLSilk[], InferSelectModel[]>, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} export interface UpdateMutationReturningSuccess< TTable extends Table, TInputI = UpdateArgs, > extends MutationFactoryWithResolve< - UpdateArgs, + UpdateOptions, GraphQLSilk, - GraphQLSilk, TInputI> + GraphQLSilk, TInputI> > {} +export interface UpdateOptions { + where?: SQL + set: Partial> +} + export type DeleteMutation> = | DeleteMutationReturningItems | DeleteMutationReturningSuccess @@ -239,31 +334,37 @@ export interface DeleteMutationReturningItems< TTable extends Table, TInputI = DeleteArgs, > extends MutationFactoryWithResolve< - DeleteArgs, + DeleteOptions, GraphQLSilk[], InferSelectModel[]>, - GraphQLSilk, TInputI> + GraphQLSilk > {} export interface DeleteMutationReturningSuccess< TTable extends Table, TInputI = DeleteArgs, > extends MutationFactoryWithResolve< - DeleteArgs, + DeleteOptions, GraphQLSilk, - GraphQLSilk, TInputI> + GraphQLSilk > {} +export interface DeleteOptions { + where?: SQL +} + export type QueryBuilder< TDatabase extends BaseDatabase, - TTableName extends keyof TDatabase["_"]["schema"], -> = TDatabase["query"] extends { [key in TTableName]: any } - ? TDatabase["query"][TTableName] + TTable extends Table, +> = TDatabase["query"] extends { + [key in InferTableTsName]: any +} + ? TDatabase["query"][InferTableTsName] : never export type AnyQueryBuilder = | MySqlRelationalQueryBuilder | PgRelationalQueryBuilder - | SQLiteRelationalQueryBuilder + | SQLiteRelationalQueryBuilder export type InferTableRelationalConfig = TQueryBuilder extends MySqlRelationalQueryBuilder< @@ -278,7 +379,6 @@ export type InferTableRelationalConfig = > ? TTableRelationalConfig : TQueryBuilder extends SQLiteRelationalQueryBuilder< - any, any, any, infer TTableRelationalConfig @@ -287,18 +387,53 @@ export type InferTableRelationalConfig = : never export type BaseDatabase = - | BaseSQLiteDatabase - | PgDatabase - | MySqlDatabase + | BaseSQLiteDatabase + | PgDatabase + | MySqlDatabase + +export type InferTablesConfig = + TDatabase extends BaseSQLiteDatabase< + any, + any, + any, + any, + infer TTablesConfig, + any + > + ? TTablesConfig + : TDatabase extends PgDatabase + ? TTablesConfig + : TDatabase extends MySqlDatabase< + any, + any, + any, + any, + infer TTablesConfig, + any + > + ? TTablesConfig + : never + +export type InferTableTsName< + TDatabase extends BaseDatabase, + TTable extends Table, +> = Extract< + ValueOf>, + { dbName: TTable["_"]["name"] } +>["tsName"] export type InferTableName = TTable["_"]["name"] export type InferRelationTable< TDatabase extends BaseDatabase, TTable extends Table, - TRelationName extends keyof InferTableRelationalConfig< - QueryBuilder> - >["relations"], -> = TDatabase["_"]["fullSchema"][InferTableRelationalConfig< - QueryBuilder> ->["relations"][TRelationName]["referencedTableName"]] + TTargetTableName extends + keyof TDatabase["_"]["relations"]["config"][TTable["_"]["name"]], +> = Extract< + ValueOf>, + { + dbName: TDatabase["_"]["relations"]["config"][TTable["_"]["name"]][TTargetTableName]["targetTable"]["_"]["name"] + } +>["table"] + +type ValueOf = T[keyof T] diff --git a/packages/drizzle/src/helper.ts b/packages/drizzle/src/helper.ts index 8d479a45..81fe070d 100644 --- a/packages/drizzle/src/helper.ts +++ b/packages/drizzle/src/helper.ts @@ -12,11 +12,24 @@ import { getTableName, sql, } from "drizzle-orm" +import { + MySqlTable, + getTableConfig as getMySQLTableConfig, +} from "drizzle-orm/mysql-core" +import { + PgTable, + getTableConfig as getPgTableConfig, +} from "drizzle-orm/pg-core" +import { + SQLiteTable, + getTableConfig as getSQLiteTableConfig, +} from "drizzle-orm/sqlite-core" +import type { GraphQLResolveInfo } from "graphql" import type { - DrizzleFactoryInputVisibilityBehaviors, + ColumnBehavior, + DrizzleFactoryInputBehaviors, SelectedTableColumns, ValueOrGetter, - VisibilityBehavior, } from "./types" /** @@ -25,14 +38,15 @@ import type { */ export function inArrayMultiple( columns: Column[], - values: readonly unknown[][] + values: readonly unknown[][], + table: any ): SQL { // Early return for empty values if (values.length === 0) return sql`FALSE` // Create (col1, col2, ...) part const columnsPart = sql`(${sql.join( - columns.map((c) => sql`${c}`), + columns.map((c) => sql`${table[c.name]}`), sql`, ` )})` @@ -65,20 +79,22 @@ export function getEnumNameByColumn(column: Column): string | undefined { export function isColumnVisible( columnName: string, - options: DrizzleFactoryInputVisibilityBehaviors, - behavior: keyof VisibilityBehavior + options: DrizzleFactoryInputBehaviors
, + behavior: keyof ColumnBehavior ): boolean { // Get specific column configuration const columnConfig = options?.[columnName as keyof typeof options] - // Get global default configuration + // Get table default configuration const defaultConfig = options?.["*"] if (columnConfig != null) { if (typeof columnConfig === "boolean") { return columnConfig } - const specificBehavior = columnConfig[behavior] - if (specificBehavior != null) { - return specificBehavior + if (!("~standard" in columnConfig)) { + const specificBehavior = columnConfig[behavior] + if (specificBehavior != null) { + return specificBehavior !== false + } } } @@ -88,7 +104,7 @@ export function isColumnVisible( } const defaultBehavior = defaultConfig[behavior] if (defaultBehavior != null) { - return defaultBehavior + return defaultBehavior !== false } } @@ -112,7 +128,10 @@ export function getSelectedColumns( table: TTable, payload: ResolverPayload | (ResolverPayload | undefined)[] | undefined ): SelectedTableColumns { - if (!payload) { + if ( + !payload || + (Array.isArray(payload) && payload.filter(Boolean).length === 0) + ) { return getTableColumns(table) as SelectedTableColumns } let selectedFields = new Set() @@ -133,3 +152,60 @@ export function getSelectedColumns( return mapValue.SKIP }) as SelectedTableColumns } + +const tablePrimaryKeys = new WeakMap() + +export function getPrimaryColumns( + table: Table +): [key: string, column: Column][] { + const cached = tablePrimaryKeys.get(table) + if (cached) return cached + let primaryColumns = Object.entries(getTableColumns(table)).filter( + ([_, col]) => col.primary + ) + if (primaryColumns.length === 0) { + let primaryKey + if (table instanceof SQLiteTable) { + primaryKey = getSQLiteTableConfig(table).primaryKeys[0] + } else if (table instanceof MySqlTable) { + primaryKey = getMySQLTableConfig(table).primaryKeys[0] + } else if (table instanceof PgTable) { + primaryKey = getPgTableConfig(table).primaryKeys[0] + } + const colToKey = new Map( + Object.entries(getTableColumns(table)).map(([key, col]) => [ + col.name, + key, + ]) + ) + const cols = new Map( + Object.values(getTableColumns(table)).map((col) => [col.name, col]) + ) + + if (primaryKey) { + primaryColumns = primaryKey.columns.map((col) => [ + colToKey.get(col.name)!, + cols.get(col.name)!, + ]) + } + } + if (primaryColumns.length === 0) { + throw new Error(`No primary key found for table ${getTableName(table)}`) + } + tablePrimaryKeys.set(table, primaryColumns) + return primaryColumns +} + +export function getParentPath(info: GraphQLResolveInfo): string { + let path = "" + let prev = info.path.prev + while (prev) { + path = path ? `${pathKey(prev)}.${path}` : pathKey(prev) + prev = prev.prev + } + return path +} + +export function pathKey(path: GraphQLResolveInfo["path"]): string { + return typeof path.key === "number" ? `[n]` : path.key +} diff --git a/packages/drizzle/src/types.ts b/packages/drizzle/src/types.ts index 4a95a8d1..67aacc47 100644 --- a/packages/drizzle/src/types.ts +++ b/packages/drizzle/src/types.ts @@ -1,4 +1,4 @@ -import { SYMBOLS, type WeaverConfig } from "@gqloom/core" +import { type GraphQLSilk, SYMBOLS, type WeaverConfig } from "@gqloom/core" import type { Column, InferSelectModel, SQL, Table } from "drizzle-orm" import type { GraphQLFieldConfig, @@ -27,12 +27,12 @@ export interface DrizzleWeaverConfig */ export interface DrizzleResolverFactoryOptions { /** - * Config the visibility behavior of the columns + * Config the behavior of the columns */ - input: DrizzleFactoryInputVisibilityBehaviors + input: DrizzleFactoryInputBehaviors } -export interface VisibilityBehavior { +export interface ColumnBehavior { /** * Is this column visible in the filters? */ @@ -41,20 +41,24 @@ export interface VisibilityBehavior { /** * Is this column visible in the insert mutation input? */ - insert?: boolean + insert?: boolean | GraphQLSilk /** * Is this column visible in the update mutation input? */ - update?: boolean + update?: boolean | GraphQLSilk } -export type DrizzleFactoryInputVisibilityBehaviors = { - [K in keyof TTable["_"]["columns"]]?: VisibilityBehavior | boolean | undefined +export type DrizzleFactoryInputBehaviors = { + [K in keyof TTable["_"]["columns"]]?: + | ColumnBehavior + | GraphQLSilk + | boolean + | undefined } & { /** * Config the default visibility behavior of all columns */ - "*"?: VisibilityBehavior | boolean | undefined + "*"?: ColumnBehavior | boolean | undefined } export interface DrizzleSilkConfig diff --git a/packages/drizzle/test/context.spec.ts b/packages/drizzle/test/context.spec.ts index 5017a779..ce685b43 100644 --- a/packages/drizzle/test/context.spec.ts +++ b/packages/drizzle/test/context.spec.ts @@ -6,7 +6,7 @@ import { GraphQLString } from "graphql" import { execute, parse } from "graphql" import { afterEach, beforeAll, describe, expect, it } from "vitest" import { useSelectedColumns } from "../src/context" -import { user } from "./schema/sqlite" +import { users } from "./schema/sqlite" describe("useSelectedColumns", () => { const selectedColumns = new Set() @@ -18,18 +18,18 @@ describe("useSelectedColumns", () => { }, }, }) - const r = resolver.of(user, { - users: query(user.$list()).resolve(() => { - for (const column of Object.keys(useSelectedColumns(user))) { + const r = resolver.of(users, { + users: query(users.$list()).resolve(() => { + for (const column of Object.keys(useSelectedColumns(users))) { selectedColumns.add(column) } const one = 1 const two = 2 if (one + two < one) { // it should be able to select all columns without using useSelectedColumns - return db.select().from(user) + return db.select().from(users) } - return db.select(useSelectedColumns(user)).from(user) + return db.select(useSelectedColumns(users)).from(users) }), greeting: field(silk(GraphQLString)) @@ -41,7 +41,7 @@ describe("useSelectedColumns", () => { beforeAll(async () => { await db.run(sql` - CREATE TABLE user ( + CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, @@ -50,7 +50,7 @@ describe("useSelectedColumns", () => { }) afterEach(async () => { - await db.delete(user) + await db.delete(users) logs = [] }) @@ -63,14 +63,14 @@ describe("useSelectedColumns", () => { } } `) - await db.insert(user).values({ id: 1, name: "John" }) + await db.insert(users).values({ id: 1, name: "John" }) logs = [] const result = await execute({ schema, document: query }) expect(selectedColumns).toEqual(new Set(["id", "name"])) expect(result.data?.users).toEqual([{ id: 1, name: "John" }]) expect(logs).toMatchInlineSnapshot(` [ - "select "id", "name" from "user"", + "select "id", "name" from "users"", ] `) }) @@ -84,13 +84,13 @@ describe("useSelectedColumns", () => { } } `) - await db.insert(user).values({ id: 1, name: "John" }) + await db.insert(users).values({ id: 1, name: "John" }) logs = [] const result = await execute({ schema, document: query }) expect(result.data?.users).toEqual([{ id: 1, greeting: "Hello John" }]) expect(logs).toMatchInlineSnapshot(` [ - "select "id", "name" from "user"", + "select "id", "name" from "users"", ] `) }) diff --git a/packages/drizzle/test/helper.spec.ts b/packages/drizzle/test/helper.spec.ts index 2a19a493..bebe1a4a 100644 --- a/packages/drizzle/test/helper.spec.ts +++ b/packages/drizzle/test/helper.spec.ts @@ -8,12 +8,15 @@ import { GraphQLString, execute, parse } from "graphql" import { beforeEach, describe, expect, it } from "vitest" import { getEnumNameByColumn, + getPrimaryColumns, getSelectedColumns, getValue, inArrayMultiple, isColumnVisible, } from "../src/helper" -import type { DrizzleFactoryInputVisibilityBehaviors } from "../src/types" +import type { DrizzleFactoryInputBehaviors } from "../src/types" +import * as mysqlTables from "./schema/mysql" +import * as pgTables from "./schema/postgres" import * as sqliteTables from "./schema/sqlite" describe("getEnumNameByColumn", () => { @@ -45,14 +48,14 @@ describe("helper", () => { it("should handle empty values", () => { const columns: Column[] = [] const values: unknown[][] = [] - const result = inArrayMultiple(columns, values) + const result = inArrayMultiple(columns, values, {}) expect(result).toEqual(sql`FALSE`) }) }) describe("isColumnVisible", () => { it("should handle boolean configuration", () => { - const options: DrizzleFactoryInputVisibilityBehaviors
= { + const options: DrizzleFactoryInputBehaviors
= { "*": true, column1: false, column2: { @@ -94,10 +97,10 @@ describe("helper", () => { describe("getSelectedColumns", () => { const selectedColumns = new Set() - const r = resolver.of(sqliteTables.user, { - users: query(sqliteTables.user.$list()).resolve((_input, payload) => { + const r = resolver.of(sqliteTables.users, { + users: query(sqliteTables.users.$list()).resolve((_input, payload) => { for (const column of Object.keys( - getSelectedColumns(sqliteTables.user, payload) + getSelectedColumns(sqliteTables.users, payload) )) { selectedColumns.add(column) } @@ -138,3 +141,32 @@ describe("getSelectedColumns", () => { expect(selectedColumns).toEqual(new Set(["id", "name"])) }) }) + +describe("getPrimaryColumns", () => { + it("should return the primary columns for a table", () => { + expect(getPrimaryColumns(sqliteTables.users)).toEqual([ + ["id", sqliteTables.users.id], + ]) + }) + + it("should return the primary columns for a sqlite table with a composite primary key", () => { + expect(getPrimaryColumns(sqliteTables.userStarPosts)).toEqual([ + ["userId", sqliteTables.userStarPosts.userId], + ["postId", sqliteTables.userStarPosts.postId], + ]) + }) + + it("should return the primary columns for a mysql table with a composite primary key", () => { + expect(getPrimaryColumns(mysqlTables.userStarPosts)).toEqual([ + ["userId", mysqlTables.userStarPosts.userId], + ["postId", mysqlTables.userStarPosts.postId], + ]) + }) + + it("should return the primary columns for a pg table with a composite primary key", () => { + expect(getPrimaryColumns(pgTables.userStarPosts)).toEqual([ + ["userId", pgTables.userStarPosts.userId], + ["postId", pgTables.userStarPosts.postId], + ]) + }) +}) diff --git a/packages/drizzle/test/input-factory.spec.ts b/packages/drizzle/test/input-factory.spec.ts index b58068c8..b68e734c 100644 --- a/packages/drizzle/test/input-factory.spec.ts +++ b/packages/drizzle/test/input-factory.spec.ts @@ -1,8 +1,11 @@ +import { field, initWeaverContext, provideWeaverContext } from "@gqloom/core" +import { ValibotWeaver } from "@gqloom/valibot" import * as pg from "drizzle-orm/pg-core" import { GraphQLScalarType, printType } from "graphql" +import * as v from "valibot" import { describe, expect, it } from "vitest" import { DrizzleInputFactory, drizzleSilk } from "../src" -import type { DrizzleFactoryInputVisibilityBehaviors } from "../src/types" +import type { DrizzleFactoryInputBehaviors } from "../src/types" describe("DrizzleInputFactory", () => { const userTable = pg.pgTable("users", { @@ -53,7 +56,9 @@ describe("DrizzleInputFactory", () => { password: PgTextFilters createdAt: PgTimestampFilters updatedAt: PgTimestampFilters - OR: [UsersFiltersOr!] + OR: [UsersFiltersNested!] + AND: [UsersFiltersNested!] + NOT: UsersFiltersNested }" `) }) @@ -109,14 +114,15 @@ describe("DrizzleInputFactory", () => { }) describe("with column visibility options", () => { - const options: DrizzleFactoryInputVisibilityBehaviors = { + const options: DrizzleFactoryInputBehaviors = { + email: v.pipe(v.string(), v.email()), "*": { filters: true, insert: true, update: true, }, password: { - filters: false, + filters: field.hidden, insert: true, update: true, }, @@ -135,10 +141,17 @@ describe("DrizzleInputFactory", () => { const inputFactoryWithOptions = new DrizzleInputFactory(userTable, { input: options, }) + const weaverContext = initWeaverContext() + weaverContext.vendorWeavers.set(ValibotWeaver.vendor, ValibotWeaver) it("should respect column visibility in InsertInput", () => { expect( - printType(inputFactoryWithOptions.insertInput()) + printType( + provideWeaverContext( + () => inputFactoryWithOptions.insertInput(), + weaverContext + ) + ) ).toMatchInlineSnapshot(` "type UsersInsertInput { id: Int @@ -151,7 +164,12 @@ describe("DrizzleInputFactory", () => { it("should respect column visibility in UpdateInput", () => { expect( - printType(inputFactoryWithOptions.updateInput()) + printType( + provideWeaverContext( + () => inputFactoryWithOptions.updateInput(), + weaverContext + ) + ) ).toMatchInlineSnapshot(` "type UsersUpdateInput { id: Int @@ -172,7 +190,9 @@ describe("DrizzleInputFactory", () => { email: PgTextFilters createdAt: PgTimestampFilters updatedAt: PgTimestampFilters - OR: [UsersFiltersOr!] + OR: [UsersFiltersNested!] + AND: [UsersFiltersNested!] + NOT: UsersFiltersNested }" `) }) diff --git a/packages/drizzle/test/resolver-factory.spec.ts b/packages/drizzle/test/resolver-factory.spec.ts index eeb037b4..13df2b34 100644 --- a/packages/drizzle/test/resolver-factory.spec.ts +++ b/packages/drizzle/test/resolver-factory.spec.ts @@ -1,5 +1,14 @@ -import { resolver } from "@gqloom/core" -import { type InferSelectModel, eq, inArray, sql } from "drizzle-orm" +import { resolver, weave } from "@gqloom/core" +import { + defineRelations, + eq, + gte, + inArray, + isNotNull, + like, + lt, + sql, +} from "drizzle-orm" import { type LibSQLDatabase, drizzle as sqliteDrizzle, @@ -12,8 +21,18 @@ import { type NodePgDatabase, drizzle as pgDrizzle, } from "drizzle-orm/node-postgres" +import * as sqlite from "drizzle-orm/sqlite-core" +import { execute, parse } from "graphql" import * as v from "valibot" -import { afterAll, beforeAll, describe, expect, expectTypeOf, it } from "vitest" +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + expectTypeOf, + it, +} from "vitest" import { config } from "../env.config" import { type DrizzleMySQLResolverFactory, @@ -23,30 +42,37 @@ import { drizzleResolverFactory, } from "../src" import type { - InferSelectArrayOptions, - InferSelectSingleOptions, + InferTableTsName, + SelectArrayOptions, + SelectSingleOptions, } from "../src/factory/types" import * as mysqlSchemas from "./schema/mysql" +import { relations as mysqlRelations } from "./schema/mysql-relations" import * as pgSchemas from "./schema/postgres" +import { relations as pgRelations } from "./schema/postgres-relations" import * as sqliteSchemas from "./schema/sqlite" +import { relations as sqliteRelations } from "./schema/sqlite-relations" const pathToDB = new URL("./schema/sqlite.db", import.meta.url) -describe.concurrent("DrizzleResolverFactory", () => { - let db: LibSQLDatabase +describe("DrizzleResolverFactory", () => { + let db: LibSQLDatabase let userFactory: DrizzleSQLiteResolverFactory< typeof db, - typeof sqliteSchemas.user + typeof sqliteSchemas.users > + let log: string[] = [] beforeAll(async () => { db = sqliteDrizzle({ - schema: sqliteSchemas, + relations: sqliteRelations, connection: { url: `file:${pathToDB.pathname}` }, + logger: { logQuery: (query) => log.push(query) }, }) - userFactory = drizzleResolverFactory(db, sqliteSchemas.user) - await db.insert(sqliteSchemas.user).values([ + userFactory = drizzleResolverFactory(db, sqliteSchemas.users) + + await db.insert(sqliteSchemas.users).values([ { name: "John", age: 10, @@ -68,25 +94,28 @@ describe.concurrent("DrizzleResolverFactory", () => { name: "Jill", age: 14, }, - ] satisfies (typeof sqliteSchemas.user.$inferInsert)[]) + ] satisfies (typeof sqliteSchemas.users.$inferInsert)[]) + }) + beforeEach(() => { + log = [] }) afterAll(async () => { - await db.delete(sqliteSchemas.user) + await db.delete(sqliteSchemas.users) }) it("should create a resolver factory", () => { expect(userFactory).toBeInstanceOf(DrizzleResolverFactory) }) - describe.concurrent("selectArrayQuery", () => { + describe("selectArrayQuery", () => { it("should be created without error", async () => { const query = userFactory.selectArrayQuery() expect(query).toBeDefined() }) it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { users: userFactory.selectArrayQuery(), }) @@ -97,17 +126,17 @@ describe.concurrent("DrizzleResolverFactory", () => { const executor = userFactory.resolver().toExecutor() let answer - answer = await executor.user({ orderBy: [{ age: "asc" }] }) + answer = await executor.users({ orderBy: { name: "asc", age: "asc" } }) expect(answer).toMatchObject([ - { age: 10 }, - { age: 11 }, - { age: 12 }, - { age: 13 }, - { age: 14 }, + { name: "Jane", age: 11 }, + { name: "Jill", age: 14 }, + { name: "Jim", age: 12 }, + { name: "Joe", age: 13 }, + { name: "John", age: 10 }, ]) - answer = await executor.user({ orderBy: [{ age: "desc" }] }) + answer = await executor.users({ orderBy: { age: "desc", name: "asc" } }) expect(answer).toMatchObject([ { age: 14 }, { age: 13 }, @@ -115,69 +144,63 @@ describe.concurrent("DrizzleResolverFactory", () => { { age: 11 }, { age: 10 }, ]) + + expect(["", ...log, ""].join("\n")).toMatchInlineSnapshot(` + " + select "id", "name", "age", "email" from "users" order by "users"."name" asc, "users"."age" asc + select "id", "name", "age", "email" from "users" order by "users"."age" desc, "users"."name" asc + " + `) }) it("should resolve correctly with filters", async () => { const executor = userFactory.resolver().toExecutor() let answer - answer = await executor.user({}) + answer = await executor.users({}) expect(answer).toHaveLength(5) - answer = await executor.user({ + answer = await executor.users({ where: { age: { gte: 12 } }, }) expect(answer).toMatchObject([{ age: 12 }, { age: 13 }, { age: 14 }]) - answer = await executor.user({ + answer = await executor.users({ where: { age: { lt: 12 } }, }) expect(answer).toMatchObject([{ age: 10 }, { age: 11 }]) - answer = await executor.user({ + answer = await executor.users({ where: { age: { gte: 12, lt: 13 } }, }) expect(answer).toMatchObject([{ age: 12 }]) - answer = await executor.user({ - where: { age: { inArray: [10, 11] } }, + answer = await executor.users({ + where: { age: { in: [10, 11] } }, }) expect(new Set(answer)).toMatchObject(new Set([{ age: 10 }, { age: 11 }])) - answer = await executor.user({ - where: { age: { notInArray: [10, 11] } }, + answer = await executor.users({ + where: { age: { notIn: [10, 11] } }, }) expect(new Set(answer)).toMatchObject( new Set([{ age: 12 }, { age: 13 }, { age: 14 }]) ) - answer = await executor.user({ + answer = await executor.users({ where: { age: { OR: [{ eq: 10 }, { eq: 11 }] } }, }) expect(new Set(answer)).toMatchObject(new Set([{ age: 10 }, { age: 11 }])) - answer = await executor.user({ + answer = await executor.users({ where: { OR: [{ age: { eq: 10 } }, { age: { eq: 11 } }] }, }) expect(new Set(answer)).toMatchObject(new Set([{ age: 10 }, { age: 11 }])) - answer = await executor.user({ + answer = await executor.users({ where: { name: { like: "J%" } }, }) expect(answer).toHaveLength(5) - await expect(() => - executor.user({ - where: { age: { eq: 10 }, OR: [{ age: { eq: 11 } }] }, - }) - ).rejects.toThrow("Cannot specify both fields and 'OR' in table filters!") - await expect(() => - executor.user({ - where: { age: { eq: 10, OR: [{ eq: 11 }] } }, - }) - ).rejects.toThrow( - "WHERE age: Cannot specify both fields and 'OR' in column operators!" - ) - - answer = await executor.user({ + answer = await executor.users({ where: { age: { isNull: true } }, }) expect(answer).toHaveLength(0) @@ -191,7 +214,7 @@ describe.concurrent("DrizzleResolverFactory", () => { age: v.nullish(v.number()), }), v.transform(({ age }) => ({ - where: age != null ? eq(sqliteSchemas.user.age, age) : undefined, + where: age != null ? eq(sqliteSchemas.users.age, age) : undefined, })) ), }) @@ -211,7 +234,7 @@ describe.concurrent("DrizzleResolverFactory", () => { age: v.nullish(v.number()), }), v.transform(({ age }) => ({ - where: age != null ? eq(sqliteSchemas.user.age, age) : undefined, + where: age != null ? eq(sqliteSchemas.users.age, age) : undefined, })) ) ) @@ -222,11 +245,6 @@ describe.concurrent("DrizzleResolverFactory", () => { }) it("should be created with middlewares", async () => { - type SelectArrayOptions = InferSelectArrayOptions< - typeof db, - typeof sqliteSchemas.user - > - let count = 0 const query = userFactory @@ -235,13 +253,12 @@ describe.concurrent("DrizzleResolverFactory", () => { async ({ parseInput, next }) => { const opts = await parseInput() if (opts.issues) throw new Error("Invalid input") - expectTypeOf(opts.value).toEqualTypeOf< - NonNullable | undefined - >() + + expectTypeOf(opts.value).toEqualTypeOf() count++ const answer = await next() expectTypeOf(answer).toEqualTypeOf< - InferSelectModel[] + (typeof sqliteSchemas.users.$inferSelect)[] >() return answer }, @@ -249,13 +266,11 @@ describe.concurrent("DrizzleResolverFactory", () => { }) .use(async ({ parseInput, next }) => { const value = await parseInput.getResult() - expectTypeOf(value).toEqualTypeOf< - NonNullable | undefined - >() + expectTypeOf(value).toEqualTypeOf() count++ const answer = await next() expectTypeOf(answer).toEqualTypeOf< - InferSelectModel[] + (typeof sqliteSchemas.users.$inferSelect)[] >() return answer }) @@ -263,11 +278,136 @@ describe.concurrent("DrizzleResolverFactory", () => { await executor.query({}) expect(count).toBe(2) }) + + it("should work with AND operators", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + let answer + answer = await executor.query({ + where: { + AND: [{ name: { eq: "John" } }, { age: { gt: 10 } }], + }, + }) + expect(answer).toHaveLength(0) + + answer = await executor.query({ + where: { + AND: [{ name: { eq: "John" } }, { age: { gte: 10 } }], + }, + }) + expect(answer).toHaveLength(1) + }) + + it("should work with OR operators", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + let answer + answer = await executor.query({ + where: { + OR: [{ name: { eq: "John" } }, { age: { gt: 12 } }], + }, + }) + expect(answer).toHaveLength(3) + + answer = await executor.query({ + where: { + OR: [{ age: { gte: 14 } }, { age: { lte: 10 } }], + }, + }) + expect(answer).toHaveLength(2) + }) + + it("should work with NOT operators", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + let answer + answer = await executor.query({ + where: { NOT: { name: { eq: "John" } } }, + }) + expect(answer).toHaveLength(4) + + answer = await executor.query({ + where: { NOT: { age: { lte: 10 } } }, + }) + expect(answer).toHaveLength(4) + }) + + it("should work with complex NOT conditions", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + let answer + + // Test NOT with OR condition + answer = await executor.query({ + where: { + NOT: { + OR: [{ name: { eq: "John" } }, { name: { eq: "Jane" } }], + } as any, + }, + }) + expect(answer).toHaveLength(3) // Should exclude both John and Jane + + // Test NOT with AND condition + answer = await executor.query({ + where: { + NOT: { + AND: [{ age: { gte: 10 } }, { age: { lte: 12 } }], + } as any, + }, + }) + // Should exclude ages 10, 11, 12 + expect(answer.map((user) => user.age).sort()).toEqual([13, 14]) + + // Test nested NOT conditions + answer = await executor.query({ + where: { NOT: { age: { lte: 12 } } }, + }) + // Double negation: NOT(NOT(age > 12)) = age > 12 + expect(answer.map((user) => user.age).sort()).toEqual([13, 14]) + }) + + it("should work with column-level NOT operator", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + // Test NOT applied to a column filter + const answer = await executor.query({ + where: { age: { NOT: { lte: 12 } } }, + }) + + // Should only include ages > 12 + expect(answer.map((user) => (user as any).age).sort()).toEqual([13, 14]) + }) + + it("should work with column-level operators (OR, AND)", async () => { + const query = userFactory.selectArrayQuery() + const executor = resolver({ query }).toExecutor() + let answer + + // Test column-level OR operator + answer = await executor.query({ + where: { + age: { + OR: [{ eq: 10 }, { eq: 11 }], + }, + }, + }) + expect(new Set(answer)).toMatchObject(new Set([{ age: 10 }, { age: 11 }])) + + // Test column-level AND operator + answer = await executor.query({ + where: { + age: { + AND: [{ gte: 10 }, { lte: 11 }], + }, + }, + }) + expect(new Set(answer)).toMatchObject(new Set([{ age: 10 }, { age: 11 }])) + }) }) - describe.concurrent("selectSingleQuery", () => { + describe("selectSingleQuery", () => { it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { user: userFactory.selectSingleQuery(), }) @@ -284,7 +424,7 @@ describe.concurrent("DrizzleResolverFactory", () => { const executor = resolver({ query }).toExecutor() expect( await executor.query({ - orderBy: [{ age: "asc" }], + orderBy: { age: "asc" }, }) ).toMatchObject({ age: 10 }) }) @@ -306,7 +446,7 @@ describe.concurrent("DrizzleResolverFactory", () => { age: v.nullish(v.number()), }), v.transform(({ age }) => ({ - where: age != null ? eq(sqliteSchemas.user.age, age) : undefined, + where: age != null ? eq(sqliteSchemas.users.age, age) : undefined, })) ), }) @@ -319,10 +459,6 @@ describe.concurrent("DrizzleResolverFactory", () => { }) it("should be created with middlewares", async () => { - type SelectSingleOptions = InferSelectSingleOptions< - typeof db, - typeof sqliteSchemas.user - > let count = 0 const query = userFactory.selectSingleQuery({ middlewares: [ @@ -330,13 +466,11 @@ describe.concurrent("DrizzleResolverFactory", () => { const opts = await parseInput() if (opts.issues) throw new Error("Invalid input") - expectTypeOf(opts.value).toEqualTypeOf< - NonNullable | undefined - >() + expectTypeOf(opts.value).toEqualTypeOf() count++ const answer = await next() expectTypeOf(answer).toEqualTypeOf< - typeof sqliteSchemas.user.$inferSelect | undefined | null + typeof sqliteSchemas.users.$inferSelect | undefined | null >() return answer }, @@ -361,22 +495,22 @@ describe.concurrent("DrizzleResolverFactory", () => { expect(answer).toBe(5) answer = await query["~meta"].resolve({ - where: { age: { gte: 12 } }, + where: gte(sqliteSchemas.users.age, 12), }) expect(answer).toBe(3) answer = await query["~meta"].resolve({ - where: { age: { lt: 12 } }, + where: lt(sqliteSchemas.users.age, 12), }) expect(answer).toBe(2) answer = await query["~meta"].resolve({ - where: { age: { inArray: [10, 11] } }, + where: inArray(sqliteSchemas.users.age, [10, 11]), }) expect(answer).toBe(2) answer = await query["~meta"].resolve({ - where: { name: { like: "J%" } }, + where: like(sqliteSchemas.users.name, "J%"), }) expect(answer).toBe(5) }) @@ -388,7 +522,7 @@ describe.concurrent("DrizzleResolverFactory", () => { age: v.nullish(v.number()), }), v.transform(({ age }) => ({ - where: age != null ? { age: { eq: age } } : undefined, + where: age != null ? eq(sqliteSchemas.users.age, age) : undefined, })) ), }) @@ -423,19 +557,19 @@ describe.concurrent("DrizzleResolverFactory", () => { describe("relationField", () => { afterAll(async () => { - await db.delete(sqliteSchemas.studentCourseGrade) - await db.delete(sqliteSchemas.studentToCourse) - await db.delete(sqliteSchemas.course) - await db.delete(sqliteSchemas.post) + await db.delete(sqliteSchemas.studentCourseGrades) + await db.delete(sqliteSchemas.studentToCourses) + await db.delete(sqliteSchemas.courses) + await db.delete(sqliteSchemas.posts) }) it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { posts: userFactory.relationField("posts"), }) - const postFactory = drizzleResolverFactory(db, sqliteSchemas.post) - const postResolver = resolver.of(sqliteSchemas.post, { + const postFactory = drizzleResolverFactory(db, sqliteSchemas.posts) + const postResolver = resolver.of(sqliteSchemas.posts, { author: postFactory.relationField("author"), }) expect(userResolver).toBeDefined() @@ -446,32 +580,103 @@ describe.concurrent("DrizzleResolverFactory", () => { const postsField = userFactory.relationField("posts").description("posts") expect(postsField).toBeDefined() - const postFactory = drizzleResolverFactory(db, "post") + const postFactory = drizzleResolverFactory(db, sqliteSchemas.posts) const authorField = postFactory .relationField("author") .description("author") expect(authorField).toBeDefined() }) + it("should be created with simple naming conventions", () => { + const users = sqlite.sqliteTable("users", { + id: sqlite.integer("id").primaryKey(), + name: sqlite.text("name"), + }) + const posts = sqlite.sqliteTable("posts", { + id: sqlite.integer("id").primaryKey(), + title: sqlite.text("title"), + authorId: sqlite.integer("authorId").references(() => users.id), + }) + + const relations = defineRelations({ users, posts }, (r) => ({ + users: { + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + })) + const db0 = sqliteDrizzle({ + relations, + connection: ":memory:", + }) + + const userFactory = drizzleResolverFactory(db0, users) + const postsField = userFactory.relationField("posts") + expect(postsField).toBeDefined() + }) + + it("should be created with complex naming conventions", () => { + const User = sqlite.sqliteTable("user", { + id: sqlite.integer("id").primaryKey(), + name: sqlite.text("name"), + }) + const Post = sqlite.sqliteTable("post", { + id: sqlite.integer("id").primaryKey(), + title: sqlite.text("title"), + authorId: sqlite.integer("authorId").references(() => User.id), + }) + + const relations = defineRelations({ users: User, posts: Post }, (r) => ({ + users: { + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + })) + const db0 = sqliteDrizzle({ + relations, + connection: ":memory:", + }) + + type postTsName = InferTableTsName + expectTypeOf("posts") + expectTypeOf>("users") + + const userFactory = drizzleResolverFactory(db0, User) + const postsField = userFactory.relationField("posts") + expect(postsField).toBeDefined() + }) + it("should resolve correctly", async () => { - const studentCourseFactory = drizzleResolverFactory(db, "studentToCourse") + const studentCourseFactory = drizzleResolverFactory( + db, + sqliteSchemas.studentToCourses + ) const gradeField = studentCourseFactory.relationField("grade") - const John = await db.query.user.findFirst({ - where: eq(sqliteSchemas.user.name, "John"), + const John = await db.query.users.findFirst({ + where: { name: "John" }, }) if (!John) throw new Error("John not found") - const Joe = await db.query.user.findFirst({ - where: eq(sqliteSchemas.user.name, "Joe"), + const Joe = await db.query.users.findFirst({ + where: { name: "Joe" }, }) if (!Joe) throw new Error("Joe not found") const [math, english] = await db - .insert(sqliteSchemas.course) + .insert(sqliteSchemas.courses) .values([{ name: "Math" }, { name: "English" }]) .returning() const studentCourses = await db - .insert(sqliteSchemas.studentToCourse) + .insert(sqliteSchemas.studentToCourses) .values([ { studentId: John.id, courseId: math.id }, { studentId: John.id, courseId: english.id }, @@ -479,20 +684,19 @@ describe.concurrent("DrizzleResolverFactory", () => { { studentId: Joe.id, courseId: english.id }, ]) .returning() - - await db.insert(sqliteSchemas.studentCourseGrade).values( + await db.insert(sqliteSchemas.studentCourseGrades).values( studentCourses.map((it) => ({ ...it, grade: Math.floor(Math.random() * 51) + 50, })) ) - let answer - answer = await Promise.all( + const answer = await Promise.all( studentCourses.map((sc) => { - return gradeField["~meta"].resolve(sc, undefined) + return gradeField["~meta"].resolve(sc, {}) }) ) + expect(new Set(answer)).toMatchObject( new Set([ { studentId: John.id, courseId: math.id }, @@ -507,36 +711,803 @@ describe.concurrent("DrizzleResolverFactory", () => { }, ]) ) + }) + + it("should resolve correctly for to-many relation", async () => { + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + const Joe = await db.query.users.findFirst({ + where: { name: "Joe" }, + }) + if (!Joe) throw new Error("Joe not found") + + const [math, english] = await db + .insert(sqliteSchemas.courses) + .values([{ name: "Math" }, { name: "English" }]) + .returning() + + const studentCourses = await db + .insert(sqliteSchemas.studentToCourses) + .values([ + { studentId: John.id, courseId: math.id }, + { studentId: John.id, courseId: english.id }, + { studentId: Joe.id, courseId: math.id }, + { studentId: Joe.id, courseId: english.id }, + ]) + .returning() + + await db.insert(sqliteSchemas.studentCourseGrades).values( + studentCourses.map((it) => ({ + ...it, + grade: Math.floor(Math.random() * 51) + 50, + })) + ) - await db.insert(sqliteSchemas.post).values([ + await db.insert(sqliteSchemas.posts).values([ { authorId: John.id, title: "Hello" }, { authorId: John.id, title: "World" }, ]) const postsField = userFactory.relationField("posts") - answer = await postsField["~meta"].resolve(John, undefined) + const answer = await postsField["~meta"].resolve(John, {}) expect(answer).toMatchObject([ { authorId: John.id }, { authorId: John.id }, ]) }) + + it("should throw an error when relation is not found", () => { + expect(() => { + userFactory.relationField("nonExistentRelation" as any) + }).toThrow( + "GQLoom-Drizzle Error: Relation users.nonExistentRelation not found in drizzle instance" + ) + }) + + it("should work with aliased relation", async () => { + const [Jane] = await db + .select() + .from(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.name, "Jane")) + .limit(1) + const [Jill] = await db + .select() + .from(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.name, "Jill")) + .limit(1) + const [Jim] = await db + .select() + .from(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.name, "Jim")) + .limit(1) + + await db.insert(sqliteSchemas.posts).values([ + { authorId: Jane.id, title: "Jane's post", reviewerId: Jim.id }, + { authorId: Jane.id, title: "Jane's post 2", reviewerId: Jim.id }, + { authorId: Jill.id, title: "Jill's post", reviewerId: Jim.id }, + { authorId: Jill.id, title: "Jill's post 2", reviewerId: Jim.id }, + { authorId: Jim.id, title: "Jim's post", reviewerId: Jill.id }, + { authorId: Jim.id, title: "Jim's post 2", reviewerId: Jim.id }, + ]) + + const userFactory = drizzleResolverFactory(db, sqliteSchemas.users) + const postsField = userFactory.relationField("posts") + const reviewedPostsField = userFactory.relationField("reviewedPosts") + let answer + answer = await postsField["~meta"].resolve(Jane, {}) + expect(answer).toMatchObject([ + { title: "Jane's post" }, + { title: "Jane's post 2" }, + ]) + answer = await postsField["~meta"].resolve(Jill, {}) + expect(answer).toMatchObject([ + { title: "Jill's post" }, + { title: "Jill's post 2" }, + ]) + answer = await postsField["~meta"].resolve(Jim, {}) + expect(answer).toMatchObject([ + { title: "Jim's post" }, + { title: "Jim's post 2" }, + ]) + answer = await reviewedPostsField["~meta"].resolve(Jane, {}) + expect(answer).toMatchObject([]) + answer = await reviewedPostsField["~meta"].resolve(Jill, {}) + expect(answer).toMatchObject([{ title: "Jim's post" }]) + answer = await reviewedPostsField["~meta"].resolve(Jim, {}) + expect(answer).toMatchObject([ + { title: "Jane's post" }, + { title: "Jane's post 2" }, + { title: "Jill's post" }, + { title: "Jill's post 2" }, + { title: "Jim's post 2" }, + ]) + }) + + it("should handle limit and offset for to-many relation", async () => { + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + await db + .delete(sqliteSchemas.posts) + .where(eq(sqliteSchemas.posts.authorId, John.id)) + + await db.insert(sqliteSchemas.posts).values([ + { authorId: John.id, title: "Post 1" }, + { authorId: John.id, title: "Post 2" }, + { authorId: John.id, title: "Post 3" }, + { authorId: John.id, title: "Post 4" }, + { authorId: John.id, title: "Post 5" }, + ]) + + const postsField = userFactory.relationField("posts") + + let answer = await postsField["~meta"].resolve(John, { + limit: 2, + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(2) + expect(answer.map((p) => p.title)).toEqual(["Post 1", "Post 2"]) + + answer = await postsField["~meta"].resolve(John, { + limit: 100, + offset: 2, + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(3) + expect(answer.map((p) => p.title)).toEqual(["Post 3", "Post 4", "Post 5"]) + + answer = await postsField["~meta"].resolve(John, { + limit: 2, + offset: 1, + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(2) + expect(answer.map((p) => p.title)).toEqual(["Post 2", "Post 3"]) + }) + + it("should handle orderBy for to-many relation", async () => { + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + await db + .delete(sqliteSchemas.posts) + .where(eq(sqliteSchemas.posts.authorId, John.id)) + + await db.insert(sqliteSchemas.posts).values([ + { authorId: John.id, title: "Post C" }, + { authorId: John.id, title: "Post A" }, + { authorId: John.id, title: "Post B" }, + ]) + + const postsField = userFactory.relationField("posts") + + let answer = await postsField["~meta"].resolve(John, { + orderBy: { title: "asc" }, + }) + expect(answer.map((p) => p.title)).toEqual(["Post A", "Post B", "Post C"]) + + answer = await postsField["~meta"].resolve(John, { + orderBy: { title: "desc" }, + }) + expect(answer.map((p) => p.title)).toEqual(["Post C", "Post B", "Post A"]) + }) + + it("should handle where for to-many relation", async () => { + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + await db + .delete(sqliteSchemas.posts) + .where(eq(sqliteSchemas.posts.authorId, John.id)) + + // Insert test posts with different titles and content + await db.insert(sqliteSchemas.posts).values([ + { authorId: John.id, title: "JavaScript Guide", content: "JS content" }, + { + authorId: John.id, + title: "TypeScript Tutorial", + content: "TS content", + }, + { + authorId: John.id, + title: "Python Basics", + content: "Python content", + }, + { + authorId: John.id, + title: "Java Programming", + content: "Java content", + }, + ]) + + const postsField = userFactory.relationField("posts") + + // First test: verify basic functionality without where clause works + let answer = await postsField["~meta"].resolve(John, { + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(4) + + // Test filtering by title containing "Script" + answer = await postsField["~meta"].resolve(John, { + where: (p) => like(p.title, "%Script%"), + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(2) + expect(answer.map((p) => p.title)).toEqual([ + "JavaScript Guide", + "TypeScript Tutorial", + ]) + + // Test filtering by content containing "content" + answer = await postsField["~meta"].resolve(John, { + where: (p) => like(p.content, "%content"), + orderBy: { title: "asc" }, + }) + expect(answer).toHaveLength(4) + + // Test filtering by exact title + answer = await postsField["~meta"].resolve(John, { + where: (p) => eq(p.title, "Python Basics"), + }) + expect(answer).toHaveLength(1) + expect(answer[0].title).toBe("Python Basics") + + // Test combining where with limit + answer = await postsField["~meta"].resolve(John, { + where: (p) => like(p.title, "%a%"), + orderBy: { title: "asc" }, + limit: 2, + }) + expect(answer).toHaveLength(2) + expect(answer.map((p) => p.title)).toEqual([ + "Java Programming", + "JavaScript Guide", + ]) + + // Test where with no matches + answer = await postsField["~meta"].resolve(John, { + where: (p) => like(p.title, "%NonExistent%"), + }) + expect(answer).toHaveLength(0) + }) + + it("should handle where for to-one relation", async () => { + const postFactory = drizzleResolverFactory(db, sqliteSchemas.posts) + const authorField = postFactory.relationField("author") + + // Create users with different names and ages + const [Alice, Bob, Charlie] = await db + .insert(sqliteSchemas.users) + .values([ + { name: "Alice", age: 25, email: "alice@example.com" }, + { name: "Bob", age: 30, email: "bob@example.com" }, + { name: "Charlie", age: 35 }, + ]) + .returning() + + // Create posts authored by these users + const [post1, post2, post3] = await db + .insert(sqliteSchemas.posts) + .values([ + { authorId: Alice.id, title: "Alice's Post" }, + { authorId: Bob.id, title: "Bob's Post" }, + { authorId: Charlie.id, title: "Charlie's Post" }, + ]) + .returning() + + try { + // Test filtering author by age >= 30 + let author = await authorField["~meta"].resolve(post1, { + where: (u) => gte(u.age, 30), + }) + expect(author).toBeNull() // Alice is 25, should not match + + author = await authorField["~meta"].resolve(post2, { + where: (u) => gte(u.age, 30), + }) + expect(author).toMatchObject({ name: "Bob", age: 30 }) + + author = await authorField["~meta"].resolve(post3, { + where: (u) => gte(u.age, 30), + }) + expect(author).toMatchObject({ name: "Charlie", age: 35 }) + + // Test filtering author by email not null + author = await authorField["~meta"].resolve(post1, { + where: (u) => isNotNull(u.email), + }) + expect(author).toMatchObject({ + name: "Alice", + email: "alice@example.com", + }) + + author = await authorField["~meta"].resolve(post3, { + where: (u) => isNotNull(u.email), + }) + expect(author).toBeNull() // Charlie has no email + + // Test filtering author by name pattern + author = await authorField["~meta"].resolve(post2, { + where: (u) => like(u.name, "B%"), + }) + expect(author).toMatchObject({ name: "Bob" }) + + author = await authorField["~meta"].resolve(post1, { + where: (u) => like(u.name, "B%"), + }) + expect(author).toBeNull() // Alice doesn't match pattern + + // Test filtering with exact match + author = await authorField["~meta"].resolve(post3, { + where: (u) => eq(u.name, "Charlie"), + }) + expect(author).toMatchObject({ name: "Charlie", age: 35 }) + + author = await authorField["~meta"].resolve(post3, { + where: (u) => eq(u.name, "NotCharlie"), + }) + expect(author).toBeNull() + } finally { + // Clean up test data + await db + .delete(sqliteSchemas.posts) + .where( + inArray(sqliteSchemas.posts.id, [post1.id, post2.id, post3.id]) + ) + await db + .delete(sqliteSchemas.users) + .where( + inArray(sqliteSchemas.users.id, [Alice.id, Bob.id, Charlie.id]) + ) + } + }) + }) + + describe("relationField with multiple relation field", () => { + let John: typeof sqliteSchemas.users.$inferSelect + let Jane: typeof sqliteSchemas.users.$inferSelect + let Jill: typeof sqliteSchemas.users.$inferSelect + beforeAll(async () => { + John = (await db.query.users.findFirst({ + where: { name: "John" }, + }))! + Jane = (await db.query.users.findFirst({ + where: { name: "Jane" }, + }))! + Jill = (await db.query.users.findFirst({ + where: { name: "Jill" }, + }))! + const posts = await db + .insert(sqliteSchemas.posts) + .values([ + { authorId: John.id, title: "John's post", reviewerId: Jane.id }, + { authorId: John.id, title: "John's post 2", reviewerId: Jane.id }, + { authorId: Jane.id, title: "Jane's post", reviewerId: John.id }, + { authorId: Jane.id, title: "Jane's post 2", reviewerId: John.id }, + { authorId: Jill.id, title: "Jill's post", reviewerId: John.id }, + { authorId: Jill.id, title: "Jill's post 2", reviewerId: John.id }, + ]) + .returning() + await db.insert(sqliteSchemas.userStarPosts).values([ + { userId: John.id, postId: posts[0].id }, + { userId: Jane.id, postId: posts[1].id }, + { userId: Jill.id, postId: posts[2].id }, + { userId: John.id, postId: posts[3].id }, + { userId: Jane.id, postId: posts[4].id }, + { userId: Jill.id, postId: posts[5].id }, + ]) + }) + + afterAll(async () => { + await db.delete(sqliteSchemas.userStarPosts) + await db.delete(sqliteSchemas.posts) + }) + + it("should aggregate relation fields", async () => { + const userFactory = drizzleResolverFactory(db, sqliteSchemas.users) + const userStarPostsFactory = drizzleResolverFactory( + db, + sqliteSchemas.userStarPosts + ) + const schema = weave( + userFactory.resolver(), + userStarPostsFactory.resolver() + ) + + const { data } = await execute({ + schema, + contextValue: {}, + document: parse(/* GraphQL */ ` + query { + users(where: { name: { in: ["John", "Jane", "Jill"] } }) { + name + posts { + title + } + reviewedPosts { + title + } + starredPosts { + post { + title + } + } + } + } + `), + }) + + expect(data).toMatchInlineSnapshot(` + { + "users": [ + { + "name": "John", + "posts": [ + { + "title": "John's post", + }, + { + "title": "John's post 2", + }, + ], + "reviewedPosts": [ + { + "title": "Jane's post", + }, + { + "title": "Jane's post 2", + }, + { + "title": "Jill's post", + }, + { + "title": "Jill's post 2", + }, + ], + "starredPosts": [ + { + "post": { + "title": "John's post", + }, + }, + { + "post": { + "title": "Jane's post 2", + }, + }, + ], + }, + { + "name": "Jane", + "posts": [ + { + "title": "Jane's post", + }, + { + "title": "Jane's post 2", + }, + ], + "reviewedPosts": [ + { + "title": "John's post", + }, + { + "title": "John's post 2", + }, + ], + "starredPosts": [ + { + "post": { + "title": "John's post 2", + }, + }, + { + "post": { + "title": "Jill's post", + }, + }, + ], + }, + { + "name": "Jill", + "posts": [ + { + "title": "Jill's post", + }, + { + "title": "Jill's post 2", + }, + ], + "reviewedPosts": [], + "starredPosts": [ + { + "post": { + "title": "Jane's post", + }, + }, + { + "post": { + "title": "Jill's post 2", + }, + }, + ], + }, + ], + } + `) + + expect(log).toMatchInlineSnapshot(` + [ + "select "id", "name" from "users" where "users"."name" in (?, ?, ?)", + "select "d0"."id" as "id", coalesce((select json_group_array(json_object('title', "title", 'id', "id")) as "r" from (select "d1"."title" as "title", "d1"."id" as "id" from "posts" as "d1" where "d0"."id" = "d1"."authorId") as "t"), jsonb_array()) as "posts", coalesce((select json_group_array(json_object('title', "title", 'id', "id")) as "r" from (select "d1"."title" as "title", "d1"."id" as "id" from "posts" as "d1" where "d0"."id" = "d1"."reviewerId") as "t"), jsonb_array()) as "reviewedPosts", coalesce((select json_group_array(json_object('postId', "postId", 'userId', "userId")) as "r" from (select "d1"."postId" as "postId", "d1"."userId" as "userId" from "userStarPosts" as "d1" where "d0"."id" = "d1"."userId") as "t"), jsonb_array()) as "starredPosts" from "users" as "d0" where "d0"."id" in (?, ?, ?)", + "select "d0"."userId" as "userId", "d0"."postId" as "postId", (select json_object('title', "title", 'id', "id") as "r" from (select "d1"."title" as "title", "d1"."id" as "id" from "posts" as "d1" where "d0"."postId" = "d1"."id" limit ?) as "t") as "post" from "userStarPosts" as "d0" where ("d0"."userId", "d0"."postId") IN ((?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?))", + ] + `) + }) + }) + + describe("relationField with multiple field relations", () => { + afterAll(async () => { + await db.delete(sqliteSchemas.studentCourseGrades) + await db.delete(sqliteSchemas.studentToCourses) + await db.delete(sqliteSchemas.courses) + }) + + it("should handle multi-field relations correctly", async () => { + // This test specifically targets the multi-field relation handling in relationField + const studentCourseFactory = drizzleResolverFactory( + db, + sqliteSchemas.studentToCourses + ) + + // Setup test data + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + const [math, english] = await db + .insert(sqliteSchemas.courses) + .values([{ name: "Math" }, { name: "English" }]) + .returning() + + // Insert multiple student-course relationships for the same student + const studentCourses = await db + .insert(sqliteSchemas.studentToCourses) + .values([ + { studentId: John.id, courseId: math.id }, + { studentId: John.id, courseId: english.id }, + ]) + .returning() + + // Test loading multiple relations at once + const courseField = studentCourseFactory.relationField("course") + const results = await Promise.all( + studentCourses.map((sc) => courseField["~meta"].resolve(sc, {})) + ) + + expect(results).toMatchObject([ + { id: math.id, name: "Math" }, + { id: english.id, name: "English" }, + ]) + + // Test with batch loading multiple parents + const studentField = studentCourseFactory.relationField("student") + const studentResults = await Promise.all( + studentCourses.map((sc) => studentField["~meta"].resolve(sc, {})) + ) + + expect(studentResults).toMatchObject([ + { id: John.id, name: "John" }, + { id: John.id, name: "John" }, + ]) + }) + + it("should handle loading relation data correctly when using multiple fields", async () => { + const studentCourseFactory = drizzleResolverFactory( + db, + sqliteSchemas.studentToCourses + ) + + // Setup test data for multiple students + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + const Joe = await db.query.users.findFirst({ + where: { name: "Joe" }, + }) + if (!Joe) throw new Error("Joe not found") + + const [math, english] = await db + .insert(sqliteSchemas.courses) + .values([{ name: "Math" }, { name: "English" }]) + .returning() + + // Insert relationships for multiple students + const studentCourses = await db + .insert(sqliteSchemas.studentToCourses) + .values([ + { studentId: John.id, courseId: math.id }, + { studentId: John.id, courseId: english.id }, + { studentId: Joe.id, courseId: math.id }, + { studentId: Joe.id, courseId: english.id }, + ]) + .returning() + + // Use the loader to fetch multiple course relations at once + const executor = studentCourseFactory.resolver().toExecutor() + + // Load all courses for all student-course relationships at once + const allResults = await Promise.all( + studentCourses.map((sc) => executor.course(sc, {})) + ) + + // Verify results include both Math and English courses + expect(allResults.map((course) => (course as any).name).sort()).toEqual([ + "English", + "English", + "Math", + "Math", + ]) + + // Test the student relationship in same batch + const studentField = studentCourseFactory.relationField("student") + const studentResults = await Promise.all( + studentCourses.map((sc) => studentField["~meta"].resolve(sc, {})) + ) + + // Verify results show both John and Joe + const studentNames = studentResults + .map((student) => (student as any).name) + .sort() + expect(studentNames).toEqual(["Joe", "Joe", "John", "John"]) + }) + + it("should handle composite key relations correctly", async () => { + // Create a mock implementation to test the multi-field relation handling directly + + // First, we'll manually mock the relation data to test the specific code paths + const mockRelation = { + sourceColumns: [{ name: "studentId" }, { name: "courseId" }], + targetColumns: [{ name: "studentId" }, { name: "courseId" }], + } + + // Manual test for getKeyByField with multiple fields + const getKeyByField = (parent: any) => { + const fieldsLength = mockRelation.sourceColumns.length + if (fieldsLength === 1) { + return parent[mockRelation.sourceColumns[0].name] + } + return mockRelation.sourceColumns + .map((field) => parent[field.name]) + .join("-") + } + + // Test with composite keys + const parentWithCompositeKey = { studentId: 1, courseId: 2 } + expect(getKeyByField(parentWithCompositeKey)).toBe("1-2") + + // Manual test for getKeyByReference with multiple fields + const getKeyByReference = (item: any) => { + const fieldsLength = mockRelation.targetColumns.length + if (fieldsLength === 1) { + return item[mockRelation.targetColumns[0].name] + } + return mockRelation.targetColumns + .map((reference) => item[reference.name]) + .join("-") + } + + // Test with composite keys for reference + const itemWithCompositeKey = { studentId: 1, courseId: 2, grade: 95 } + expect(getKeyByReference(itemWithCompositeKey)).toBe("1-2") + + // Now, create and test real data to verify the full flow + const studentCourseFactory = drizzleResolverFactory( + db, + sqliteSchemas.studentToCourses + ) + + // Setup test data for a composite key scenario + // First create some test data + const John = await db.query.users.findFirst({ + where: { name: "John" }, + }) + if (!John) throw new Error("John not found") + + const Jane = await db.query.users.findFirst({ + where: { name: "Jane" }, + }) + if (!Jane) throw new Error("Jane not found") + + // Insert courses if needed + const [math, english] = await db + .insert(sqliteSchemas.courses) + .values([{ name: "Math" }, { name: "English" }]) + .returning() + + // Create student-to-course mappings with composite keys + await db + .insert(sqliteSchemas.studentToCourses) + .values([ + { studentId: John.id, courseId: math.id }, + { studentId: John.id, courseId: english.id }, + { studentId: Jane.id, courseId: math.id }, + ]) + .returning() + + // Insert grades using the composite keys + await db.insert(sqliteSchemas.studentCourseGrades).values([ + { studentId: John.id, courseId: math.id, grade: 90 }, + { studentId: John.id, courseId: english.id, grade: 85 }, + { studentId: Jane.id, courseId: math.id, grade: 95 }, + ]) + + // Now test loading data with composite keys + const gradeField = studentCourseFactory.relationField("grade") + + // Load grades for all student-course pairs + const studentCourses = await db.query.studentToCourses.findMany({ + where: { + OR: [ + { studentId: { eq: John.id }, courseId: { eq: math.id } }, + { studentId: { eq: John.id }, courseId: { eq: english.id } }, + { studentId: { eq: Jane.id }, courseId: { eq: math.id } }, + ], + }, + }) + + expect(studentCourses.length).toBe(3) + + const grades = await Promise.all( + studentCourses.map((sc) => gradeField["~meta"].resolve(sc, {})) + ) + + // Verify we got all the grades back + expect(grades.length).toBe(3) + + // Check that each student-course pair got the correct grade + const gradeMap = new Map() + grades.forEach((g: any) => { + if (g) { + const key = `${g.studentId}-${g.courseId}` + gradeMap.set(key, g.grade) + } + }) + + expect(gradeMap.size).toBe(3) + expect(gradeMap.has(`${John.id}-${math.id}`)).toBe(true) + expect(gradeMap.has(`${John.id}-${english.id}`)).toBe(true) + expect(gradeMap.has(`${Jane.id}-${math.id}`)).toBe(true) + }) }) describe("resolver", () => { it("should be created without error", () => { const userExecutor = userFactory.resolver().toExecutor() expect(userExecutor).toBeDefined() - expect(userExecutor.user).toBeDefined() - expect(userExecutor.userSingle).toBeDefined() - expect(userExecutor.insertIntoUser).toBeDefined() - expect(userExecutor.insertIntoUserSingle).toBeDefined() - expect(userExecutor.updateUser).toBeDefined() - expect(userExecutor.deleteFromUser).toBeDefined() + expect(userExecutor.users).toBeDefined() + expect(userExecutor.usersSingle).toBeDefined() + expect(userExecutor.insertIntoUsers).toBeDefined() + expect(userExecutor.insertIntoUsersSingle).toBeDefined() + expect(userExecutor.updateUsers).toBeDefined() + expect(userExecutor.deleteFromUsers).toBeDefined() expect(userExecutor.courses).toBeDefined() expect(userExecutor.posts).toBeDefined() }) }) - describe.concurrent("queriesResolver", () => { + describe("queriesResolver", () => { it("should be created without error", async () => { const resolver = userFactory.queriesResolver() expect(resolver).toBeDefined() @@ -546,7 +1517,7 @@ describe.concurrent("DrizzleResolverFactory", () => { const executor = userFactory.queriesResolver().toExecutor() // Test array query - const arrayAnswer = await executor.user({ orderBy: [{ age: "asc" }] }) + const arrayAnswer = await executor.users({ orderBy: { age: "asc" } }) expect(arrayAnswer).toMatchObject([ { age: 10 }, { age: 11 }, @@ -556,13 +1527,13 @@ describe.concurrent("DrizzleResolverFactory", () => { ]) // Test single query - const singleAnswer = await executor.userSingle({ + const singleAnswer = await executor.usersSingle({ where: { age: { eq: 12 } }, }) expect(singleAnswer).toMatchObject({ age: 12 }) // Test count query - const countAnswer = await executor.userCount({ + const countAnswer = await executor.usersCount({ where: { age: { gte: 12 } }, }) expect(countAnswer).toBe(3) @@ -572,7 +1543,7 @@ describe.concurrent("DrizzleResolverFactory", () => { const resolver = userFactory.queriesResolver({ name: "customUser" }) const executor = resolver.toExecutor() - const answer = await executor.customUser({ orderBy: [{ age: "asc" }] }) + const answer = await executor.customUser({ orderBy: { age: "asc" } }) expect(answer).toMatchObject([ { age: 10 }, { age: 11 }, @@ -597,7 +1568,7 @@ describe.concurrent("DrizzleResolverFactory", () => { }) const executor = resolver.toExecutor() - await executor.user({ orderBy: [{ age: "asc" }] }) + await executor.users({ orderBy: { age: "asc" } }) expect(count).toBe(1) }) }) @@ -605,23 +1576,26 @@ describe.concurrent("DrizzleResolverFactory", () => { describe.concurrent("DrizzleMySQLResolverFactory", () => { const schema = { - drizzle_user: mysqlSchemas.user, + users: mysqlSchemas.users, } - let db: MySql2Database + let db: MySql2Database let userFactory: DrizzleMySQLResolverFactory< typeof db, - typeof mysqlSchemas.user + typeof mysqlSchemas.users > beforeAll(async () => { - db = mysqlDrizzle(config.mysqlUrl, { schema, mode: "default" }) - userFactory = drizzleResolverFactory(db, "drizzle_user") + db = mysqlDrizzle(config.mysqlUrl, { + relations: mysqlRelations, + mode: "default", + }) + userFactory = drizzleResolverFactory(db, mysqlSchemas.users) await db.execute(sql`select 1`) }) describe("insertArrayMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(mysqlSchemas.user, { + const userResolver = resolver.of(mysqlSchemas.users, { insertArrayMutation: userFactory.insertArrayMutation(), }) @@ -644,8 +1618,8 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { ).toMatchObject({ isSuccess: true }) await db - .delete(mysqlSchemas.user) - .where(inArray(mysqlSchemas.user.age, [5, 6])) + .delete(mysqlSchemas.users) + .where(inArray(mysqlSchemas.users.age, [5, 6])) }) it("should be created with custom input", async () => { @@ -671,14 +1645,14 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { }) await db - .delete(mysqlSchemas.user) - .where(inArray(mysqlSchemas.user.age, [5, 6])) + .delete(mysqlSchemas.users) + .where(inArray(mysqlSchemas.users.age, [5, 6])) }) }) describe("insertSingleMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(mysqlSchemas.user, { + const userResolver = resolver.of(mysqlSchemas.users, { insertSingleMutation: userFactory.insertSingleMutation(), }) @@ -697,13 +1671,13 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { }) expect(answer).toMatchObject({ isSuccess: true }) - await db.delete(mysqlSchemas.user).where(eq(mysqlSchemas.user.age, 7)) + await db.delete(mysqlSchemas.users).where(eq(mysqlSchemas.users.age, 7)) }) }) describe("updateMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(mysqlSchemas.user, { + const userResolver = resolver.of(mysqlSchemas.users, { updateMutation: userFactory.updateMutation(), }) @@ -715,23 +1689,23 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(mysqlSchemas.user).values({ name: "Bob", age: 18 }) + await db.insert(mysqlSchemas.users).values({ name: "Bob", age: 18 }) const mutation = userFactory.updateMutation() expect( await mutation["~meta"].resolve({ - where: { name: { eq: "Bob" } }, + where: eq(mysqlSchemas.users.name, "Bob"), set: { age: 19 }, }) ).toMatchObject({ isSuccess: true }) await db - .delete(mysqlSchemas.user) - .where(eq(mysqlSchemas.user.name, "Bob")) + .delete(mysqlSchemas.users) + .where(eq(mysqlSchemas.users.name, "Bob")) }) }) describe("deleteMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(mysqlSchemas.user, { + const userResolver = resolver.of(mysqlSchemas.users, { deleteMutation: userFactory.deleteMutation(), }) @@ -743,17 +1717,17 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(mysqlSchemas.user).values({ name: "Alice", age: 18 }) + await db.insert(mysqlSchemas.users).values({ name: "Alice", age: 18 }) try { const mutation = userFactory.deleteMutation() const answer = await mutation["~meta"].resolve({ - where: { name: { eq: "Alice" } }, + where: eq(mysqlSchemas.users.name, "Alice"), }) expect(answer).toMatchObject({ isSuccess: true }) } finally { await db - .delete(mysqlSchemas.user) - .where(eq(mysqlSchemas.user.name, "Alice")) + .delete(mysqlSchemas.users) + .where(eq(mysqlSchemas.users.name, "Alice")) } }) }) @@ -761,23 +1735,25 @@ describe.concurrent("DrizzleMySQLResolverFactory", () => { describe.concurrent("DrizzlePostgresResolverFactory", () => { const schema = { - drizzle_user: pgSchemas.user, + users: pgSchemas.users, } - let db: NodePgDatabase + let db: NodePgDatabase let userFactory: DrizzlePostgresResolverFactory< typeof db, - typeof pgSchemas.user + typeof pgSchemas.users > beforeAll(async () => { - db = pgDrizzle(config.postgresUrl, { schema }) - userFactory = drizzleResolverFactory(db, "drizzle_user") + db = pgDrizzle(config.postgresUrl, { + relations: pgRelations, + }) + userFactory = drizzleResolverFactory(db, pgSchemas.users) await db.execute(sql`select 1`) }) describe("insertArrayMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(pgSchemas.user, { + const userResolver = resolver.of(pgSchemas.users, { insertArrayMutation: userFactory.insertArrayMutation(), }) @@ -801,7 +1777,9 @@ describe.concurrent("DrizzlePostgresResolverFactory", () => { { name: "Jane", age: 6 }, ]) - await db.delete(pgSchemas.user).where(inArray(pgSchemas.user.age, [5, 6])) + await db + .delete(pgSchemas.users) + .where(inArray(pgSchemas.users.age, [5, 6])) }) it("should be created with custom input", async () => { @@ -828,14 +1806,14 @@ describe.concurrent("DrizzlePostgresResolverFactory", () => { ]) await db - .delete(pgSchemas.user) - .where(inArray(mysqlSchemas.user.age, [5, 6])) + .delete(pgSchemas.users) + .where(inArray(mysqlSchemas.users.age, [5, 6])) }) }) describe("insertSingleMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(pgSchemas.user, { + const userResolver = resolver.of(pgSchemas.users, { insertSingleMutation: userFactory.insertSingleMutation(), }) @@ -854,13 +1832,13 @@ describe.concurrent("DrizzlePostgresResolverFactory", () => { expect(answer).toMatchObject({ name: "John", age: 7 }) - await db.delete(pgSchemas.user).where(eq(pgSchemas.user.id, answer!.id)) + await db.delete(pgSchemas.users).where(eq(pgSchemas.users.id, answer!.id)) }) }) describe("updateMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(pgSchemas.user, { + const userResolver = resolver.of(pgSchemas.users, { updateMutation: userFactory.updateMutation(), }) @@ -872,23 +1850,23 @@ describe.concurrent("DrizzlePostgresResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(pgSchemas.user).values({ name: "Bob", age: 18 }) + await db.insert(pgSchemas.users).values({ name: "Bob", age: 18 }) try { const mutation = userFactory.updateMutation() const answer = await mutation["~meta"].resolve({ - where: { name: { eq: "Bob" } }, + where: eq(pgSchemas.users.name, "Bob"), set: { age: 19 }, }) expect(answer).toMatchObject([{ name: "Bob", age: 19 }]) } finally { - await db.delete(pgSchemas.user).where(eq(pgSchemas.user.name, "Bob")) + await db.delete(pgSchemas.users).where(eq(pgSchemas.users.name, "Bob")) } }) }) describe("deleteMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(pgSchemas.user, { + const userResolver = resolver.of(pgSchemas.users, { deleteMutation: userFactory.deleteMutation(), }) @@ -901,39 +1879,41 @@ describe.concurrent("DrizzlePostgresResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(pgSchemas.user).values({ name: "Alice", age: 18 }) + await db.insert(pgSchemas.users).values({ name: "Alice", age: 18 }) try { const mutation = userFactory.deleteMutation() const answer = await mutation["~meta"].resolve({ - where: { name: { eq: "Alice" } }, + where: eq(pgSchemas.users.name, "Alice"), }) expect(answer).toMatchObject([{ name: "Alice", age: 18 }]) } finally { - await db.delete(pgSchemas.user).where(eq(pgSchemas.user.name, "Alice")) + await db + .delete(pgSchemas.users) + .where(eq(pgSchemas.users.name, "Alice")) } }) }) }) describe.concurrent("DrizzleSQLiteResolverFactory", () => { - let db: LibSQLDatabase + let db: LibSQLDatabase let userFactory: DrizzleSQLiteResolverFactory< typeof db, - typeof sqliteSchemas.user + typeof sqliteSchemas.users > beforeAll(async () => { db = sqliteDrizzle({ - schema: sqliteSchemas, + relations: sqliteRelations, connection: { url: `file:${pathToDB.pathname}` }, }) - userFactory = drizzleResolverFactory(db, "user") + userFactory = drizzleResolverFactory(db, sqliteSchemas.users) }) describe("insertArrayMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { insertArrayMutation: userFactory.insertArrayMutation(), }) @@ -961,14 +1941,14 @@ describe.concurrent("DrizzleSQLiteResolverFactory", () => { ]) await db - .delete(sqliteSchemas.user) - .where(inArray(sqliteSchemas.user.age, [5, 6])) + .delete(sqliteSchemas.users) + .where(inArray(sqliteSchemas.users.age, [5, 6])) }) }) describe("insertSingleMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { insertSingleMutation: userFactory.insertSingleMutation(), }) @@ -988,8 +1968,8 @@ describe.concurrent("DrizzleSQLiteResolverFactory", () => { expect(answer).toMatchObject({ name: "John", age: 7 }) await db - .delete(sqliteSchemas.user) - .where(eq(sqliteSchemas.user.id, answer!.id)) + .delete(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.id, answer!.id)) }) it("should be created with custom input", async () => { @@ -1016,14 +1996,14 @@ describe.concurrent("DrizzleSQLiteResolverFactory", () => { ]) await db - .delete(sqliteSchemas.user) - .where(inArray(sqliteSchemas.user.age, [5, 6])) + .delete(sqliteSchemas.users) + .where(inArray(sqliteSchemas.users.age, [5, 6])) }) }) describe("updateMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { updateMutation: userFactory.updateMutation(), }) @@ -1036,25 +2016,25 @@ describe.concurrent("DrizzleSQLiteResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(sqliteSchemas.user).values({ name: "Bob", age: 18 }) + await db.insert(sqliteSchemas.users).values({ name: "Bob", age: 18 }) try { const mutation = userFactory.updateMutation() const answer = await mutation["~meta"].resolve({ - where: { name: { eq: "Bob" } }, + where: eq(sqliteSchemas.users.name, "Bob"), set: { age: 19 }, }) expect(answer).toMatchObject([{ name: "Bob", age: 19 }]) } finally { await db - .delete(sqliteSchemas.user) - .where(eq(sqliteSchemas.user.name, "Bob")) + .delete(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.name, "Bob")) } }) }) describe("deleteMutation", () => { it("should be used without error", () => { - const userResolver = resolver.of(sqliteSchemas.user, { + const userResolver = resolver.of(sqliteSchemas.users, { deleteMutation: userFactory.deleteMutation(), }) @@ -1067,18 +2047,18 @@ describe.concurrent("DrizzleSQLiteResolverFactory", () => { }) it("should resolve correctly", async () => { - await db.insert(sqliteSchemas.user).values({ name: "Alice", age: 18 }) + await db.insert(sqliteSchemas.users).values({ name: "Alice", age: 18 }) try { const mutation = userFactory.deleteMutation() const answer = await mutation["~meta"].resolve({ - where: { name: { eq: "Alice" } }, + where: eq(sqliteSchemas.users.name, "Alice"), }) expect(answer).toMatchObject([{ name: "Alice", age: 18 }]) } finally { await db - .delete(sqliteSchemas.user) - .where(eq(sqliteSchemas.user.name, "Alice")) + .delete(sqliteSchemas.users) + .where(eq(sqliteSchemas.users.name, "Alice")) } }) }) diff --git a/packages/drizzle/test/resolver-mysql.spec.gql b/packages/drizzle/test/resolver-mysql.spec.gql index a7dd41b2..00d71f85 100644 --- a/packages/drizzle/test/resolver-mysql.spec.gql +++ b/packages/drizzle/test/resolver-mysql.spec.gql @@ -1,12 +1,12 @@ type Mutation { - deleteFromPost(where: PostFilters): MutationSuccessResult - deleteFromUser(where: UserFilters): MutationSuccessResult - insertIntoPost(values: [PostInsertInput!]!): MutationSuccessResult - insertIntoPostSingle(value: PostInsertInput!): MutationSuccessResult - insertIntoUser(values: [UserInsertInput!]!): MutationSuccessResult - insertIntoUserSingle(value: UserInsertInput!): MutationSuccessResult - updatePost(set: PostUpdateInput!, where: PostFilters): MutationSuccessResult - updateUser(set: UserUpdateInput!, where: UserFilters): MutationSuccessResult + deleteFromPosts(where: PostFilters): MutationSuccessResult + deleteFromUsers(where: UserFilters): MutationSuccessResult + insertIntoPosts(values: [PostInsertInput!]!): MutationSuccessResult + insertIntoPostsSingle(value: PostInsertInput!): MutationSuccessResult + insertIntoUsers(values: [UserInsertInput!]!): MutationSuccessResult + insertIntoUsersSingle(value: UserInsertInput!): MutationSuccessResult + updatePosts(set: PostUpdateInput!, where: PostFilters): MutationSuccessResult + updateUsers(set: UserUpdateInput!, where: UserFilters): MutationSuccessResult } type MutationSuccessResult { @@ -14,39 +14,43 @@ type MutationSuccessResult { } input MySqlIntFilters { - OR: [MySqlIntFiltersOr!] + AND: [MySqlIntFiltersNested!] + NOT: MySqlIntFiltersNested + OR: [MySqlIntFiltersNested!] eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } -input MySqlIntFiltersOr { +input MySqlIntFiltersNested { eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } input MySqlTextFilters { - OR: [MySqlTextFiltersOr!] + AND: [MySqlTextFiltersNested!] + NOT: MySqlTextFiltersNested + OR: [MySqlTextFiltersNested!] eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -54,16 +58,16 @@ input MySqlTextFilters { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } -input MySqlTextFiltersOr { +input MySqlTextFiltersNested { eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -71,7 +75,7 @@ input MySqlTextFiltersOr { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } @@ -82,7 +86,7 @@ enum OrderDirection { """A post""" type Post { - author: User + author(where: UserFilters): User authorId: Int content: String id: Int! @@ -93,7 +97,9 @@ type Post { """A post""" input PostFilters { - OR: [PostFiltersOr!] + AND: [PostFiltersNested!] + NOT: PostFiltersNested + OR: [PostFiltersNested!] authorId: MySqlIntFilters content: MySqlTextFilters id: MySqlIntFilters @@ -102,7 +108,7 @@ input PostFilters { title: MySqlTextFilters } -input PostFiltersOr { +input PostFiltersNested { authorId: MySqlIntFilters content: MySqlTextFilters id: MySqlIntFilters @@ -141,12 +147,12 @@ input PostUpdateInput { } type Query { - post(limit: Int, offset: Int, orderBy: [PostOrderBy!], where: PostFilters): [Post!]! - postCount(where: PostFilters): Int! - postSingle(offset: Int, orderBy: [PostOrderBy!], where: PostFilters): Post - user(limit: Int, offset: Int, orderBy: [UserOrderBy!], where: UserFilters): [User!]! - userCount(where: UserFilters): Int! - userSingle(offset: Int, orderBy: [UserOrderBy!], where: UserFilters): User + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! + postsCount(where: PostFilters): Int! + postsSingle(offset: Int, orderBy: PostOrderBy, where: PostFilters): Post + users(limit: Int, offset: Int, orderBy: UserOrderBy, where: UserFilters): [User!]! + usersCount(where: UserFilters): Int! + usersSingle(offset: Int, orderBy: UserOrderBy, where: UserFilters): User } """A user""" @@ -157,12 +163,14 @@ type User { """The name of the user""" name: String! - posts: [Post!]! + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! } """A user""" input UserFilters { - OR: [UserFiltersOr!] + AND: [UserFiltersNested!] + NOT: UserFiltersNested + OR: [UserFiltersNested!] age: MySqlIntFilters email: MySqlTextFilters id: MySqlIntFilters @@ -171,7 +179,7 @@ input UserFilters { name: MySqlTextFilters } -input UserFiltersOr { +input UserFiltersNested { age: MySqlIntFilters email: MySqlTextFilters id: MySqlIntFilters diff --git a/packages/drizzle/test/resolver-mysql.spec.ts b/packages/drizzle/test/resolver-mysql.spec.ts index dcb43f4a..94464ee3 100644 --- a/packages/drizzle/test/resolver-mysql.spec.ts +++ b/packages/drizzle/test/resolver-mysql.spec.ts @@ -1,5 +1,5 @@ import { weave } from "@gqloom/core" -import { eq } from "drizzle-orm" +import { ValibotWeaver } from "@gqloom/valibot" import { drizzle } from "drizzle-orm/mysql2" import type { MySql2Database } from "drizzle-orm/mysql2" import { @@ -8,20 +8,20 @@ import { printSchema, } from "graphql" import { type YogaServerInstance, createYoga } from "graphql-yoga" +import * as v from "valibot" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { config } from "../env.config" import { drizzleResolverFactory } from "../src" -import { post, postsRelations, user, usersRelations } from "./schema/mysql" +import { posts, users } from "./schema/mysql" +import { relations } from "./schema/mysql-relations" const schema = { - drizzle_user: user, - drizzle_post: post, - usersRelations, - postsRelations, + users, + posts, } describe("resolver by mysql", () => { - let db: MySql2Database + let db: MySql2Database let logs: string[] = [] let gqlSchema: GraphQLSchema let yoga: YogaServerInstance<{}, {}> @@ -41,7 +41,7 @@ describe("resolver by mysql", () => { const { data, errors } = await response.json() if (response.status !== 200 || errors != null) { - console.info(errors) + // console.info(errors) throw new Error(JSON.stringify(errors)) } return data @@ -50,33 +50,38 @@ describe("resolver by mysql", () => { beforeAll(async () => { try { db = drizzle(config.mysqlUrl, { - schema, + relations, mode: "default", logger: { logQuery: (query) => logs.push(query) }, }) - const userFactory = drizzleResolverFactory(db, "drizzle_user") - const postFactory = drizzleResolverFactory(db, "drizzle_post") + const userFactory = drizzleResolverFactory(db, users, { + input: { + email: v.nullish(v.pipe(v.string(), v.email())), + }, + }) + const postFactory = drizzleResolverFactory(db, posts) gqlSchema = weave( - userFactory.resolver({ name: "user" }), - postFactory.resolver({ name: "post" }) + ValibotWeaver, + userFactory.resolver({ name: "users" }), + postFactory.resolver({ name: "posts" }) ) yoga = createYoga({ schema: gqlSchema }) await db - .insert(user) + .insert(users) .values([{ name: "Tom" }, { name: "Tony" }, { name: "Taylor" }]) - const Tom = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) - const Tony = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) - const Taylor = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Taylor"), + const Taylor = await db.query.users.findFirst({ + where: { name: "Taylor" }, }) if (!Tom || !Tony || !Taylor) throw new Error("User not found") - await db.insert(post).values([ + await db.insert(posts).values([ { title: "Post 1", authorId: Tom.id }, { title: "Post 2", authorId: Tony.id }, { title: "Post 3", authorId: Taylor.id }, @@ -92,8 +97,8 @@ describe("resolver by mysql", () => { }) afterAll(async () => { - await db.delete(post) - await db.delete(user) + await db.delete(posts) + await db.delete(users) }) it("should weave GraphQL schema correctly", async () => { @@ -105,47 +110,47 @@ describe("resolver by mysql", () => { describe("query", () => { it("should query users correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { id name } } - ` + ` await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], + users: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 2, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }], + users: [{ name: "Taylor" }, { name: "Tom" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 1, offset: 1, }) ).resolves.toMatchObject({ - user: [{ name: "Tom" }], + users: [{ name: "Tom" }], }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select \`id\`, \`name\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` like ? order by \`drizzle_user\`.\`name\` asc - select \`id\`, \`name\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` like ? order by \`drizzle_user\`.\`name\` asc limit ? - select \`id\`, \`name\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` like ? order by \`drizzle_user\`.\`name\` asc limit ? offset ? + select \`id\`, \`name\` from \`users\` where \`users\`.\`name\` like ? order by \`users\`.\`name\` asc + select \`id\`, \`name\` from \`users\` where \`users\`.\`name\` like ? order by \`users\`.\`name\` asc limit ? + select \`id\`, \`name\` from \`users\` where \`users\`.\`name\` like ? order by \`users\`.\`name\` asc limit ? offset ? " `) }) @@ -154,31 +159,31 @@ describe("resolver by mysql", () => { await expect( execute( /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $offset: Int) { - userSingle(orderBy: $orderBy, where: $where, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $offset: Int) { + usersSingle(orderBy: $orderBy, where: $where, offset: $offset) { id name } } - `, + `, { where: { name: { eq: "Taylor" } }, } ) ).resolves.toMatchObject({ - userSingle: { name: "Taylor" }, + usersSingle: { name: "Taylor" }, }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select \`id\`, \`name\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? + select \`id\`, \`name\` from \`users\` where \`users\`.\`name\` = ? limit ? " `) }) it("should query user with posts correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { id name posts { @@ -191,11 +196,11 @@ describe("resolver by mysql", () => { await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [ + users: [ { name: "Taylor", posts: [{ title: "Post 3" }], @@ -213,8 +218,8 @@ describe("resolver by mysql", () => { expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select \`id\`, \`name\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` like ? order by \`drizzle_user\`.\`name\` asc - select \`id\`, \`title\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`authorId\` in (?, ?, ?) + select \`id\`, \`name\` from \`users\` where \`users\`.\`name\` like ? order by \`users\`.\`name\` asc + select \`d0\`.\`id\` as \`id\`, \`posts\`.\`r\` as \`posts\` from \`users\` as \`d0\` left join lateral(select coalesce(json_arrayagg(json_object('id', \`id\`, 'title', \`title\`)), json_array()) as \`r\` from (select \`d1\`.\`id\` as \`id\`, \`d1\`.\`title\` as \`title\` from \`posts\` as \`d1\` where \`d0\`.\`id\` = \`d1\`.\`authorId\`) as \`t\`) as \`posts\` on true where \`d0\`.\`id\` in (?, ?, ?) " `) }) @@ -223,49 +228,77 @@ describe("resolver by mysql", () => { describe("mutation", () => { it("should insert a new user correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUser($values: [UserInsertInput!]!) { - insertIntoUser(values: $values) { + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { isSuccess } } - ` + ` await expect( execute(q, { values: [{ name: "Tina" }], }) ).resolves.toMatchObject({ - insertIntoUser: { isSuccess: true }, + insertIntoUsers: { isSuccess: true }, }) // Verify the user was inserted - const Tina = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tina"), + const Tina = await db.query.users.findFirst({ + where: { name: "Tina" }, }) expect(Tina).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into \`drizzle_user\` (\`id\`, \`name\`, \`age\`, \`email\`) values (default, ?, default, default) - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? + insert into \`users\` (\`id\`, \`name\`, \`age\`, \`email\`) values (default, ?, default, default) + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`name\` = ? limit ? " `) }) + it("should throw error when insert a user with invalid email", async () => { + const q1 = /* GraphQL */ ` + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { + isSuccess + } + } + ` + await expect( + execute(q1, { + values: [{ name: "Tina", email: "modevol.com" }], + }) + ).rejects.toThrow("Invalid email") + + const q2 = /* GraphQL */ ` + mutation insertIntoUsersSingle($value: UserInsertInput!) { + insertIntoUsersSingle(value: $value) { + isSuccess + } + } + ` + await expect( + execute(q2, { + value: { name: "Tina", email: "modevol.com" }, + }) + ).rejects.toThrow("Invalid email") + }) + it("should update user information correctly", async () => { const q = /* GraphQL */ ` mutation updateUser($set: UserUpdateInput!, $where: UserFilters!) { - updateUser(set: $set, where: $where) { + updateUsers(set: $set, where: $where) { isSuccess } } ` const [TroyID] = await db - .insert(user) + .insert(users) .values({ name: "Troy" }) .$returningId() - const Troy = await db.query.drizzle_user.findFirst({ - where: eq(user.id, TroyID.id), + const Troy = await db.query.users.findFirst({ + where: { id: TroyID.id }, }) if (!Troy) throw new Error("User not found") @@ -275,35 +308,55 @@ describe("resolver by mysql", () => { where: { id: { eq: Troy.id } }, }) ).resolves.toMatchObject({ - updateUser: { isSuccess: true }, + updateUsers: { isSuccess: true }, }) // Verify the user was updated - const updatedUser = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tiffany"), + const updatedUser = await db.query.users.findFirst({ + where: { name: "Tiffany" }, }) expect(updatedUser).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into \`drizzle_user\` (\`id\`, \`name\`, \`age\`, \`email\`) values (default, ?, default, default) - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`id\` = ? limit ? - update \`drizzle_user\` set \`name\` = ? where \`drizzle_user\`.\`id\` = ? - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? + insert into \`users\` (\`id\`, \`name\`, \`age\`, \`email\`) values (default, ?, default, default) + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`id\` = ? limit ? + update \`users\` set \`name\` = ? where \`users\`.\`id\` = ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`name\` = ? limit ? " `) }) + it("should throw error when update a user with invalid email", async () => { + const q = /* GraphQL */ ` + mutation updateUsers($set: UserUpdateInput!, $where: UserFilters!) { + updateUsers(set: $set, where: $where) { + isSuccess + } + } + ` + const [Danny] = await db + .insert(users) + .values({ name: "Danny" }) + .$returningId() + await expect( + execute(q, { + set: { email: "modevol.com" }, + where: { id: { eq: Danny.id } }, + }) + ).rejects.toThrow("Invalid email") + }) + it("should delete a user correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromUser($where: UserFilters!) { - deleteFromUser(where: $where) { + mutation deleteFromUsers($where: UserFilters!) { + deleteFromUsers(where: $where) { isSuccess } } ` - const Tony = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) if (!Tony) throw new Error("User not found") @@ -312,36 +365,36 @@ describe("resolver by mysql", () => { where: { id: { eq: Tony.id } }, }) ).resolves.toMatchObject({ - deleteFromUser: { + deleteFromUsers: { isSuccess: true, }, }) // Verify the user was deleted - const deletedUser = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const deletedUser = await db.query.users.findFirst({ + where: { name: "Tony" }, }) expect(deletedUser).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? - delete from \`drizzle_user\` where \`drizzle_user\`.\`id\` = ? - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`name\` = ? limit ? + delete from \`users\` where \`users\`.\`id\` = ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`name\` = ? limit ? " `) }) it("should insert a new post correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoPost($values: [PostInsertInput!]!) { - insertIntoPost(values: $values) { + mutation insertIntoPosts($values: [PostInsertInput!]!) { + insertIntoPosts(values: $values) { isSuccess } } ` - const Tom = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) if (!Tom) throw new Error("User not found") @@ -350,41 +403,41 @@ describe("resolver by mysql", () => { values: [{ title: "Post 5", authorId: Tom.id }], }) ).resolves.toMatchObject({ - insertIntoPost: { + insertIntoPosts: { isSuccess: true, }, }) // Verify the post was inserted - const p = await db.query.drizzle_post.findFirst({ - where: eq(post.title, "Post 5"), + const p = await db.query.posts.findFirst({ + where: { title: "Post 5" }, }) expect(p).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select \`id\`, \`name\`, \`age\`, \`email\` from \`drizzle_user\` where \`drizzle_user\`.\`name\` = ? limit ? - insert into \`drizzle_post\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, ?) - select \`id\`, \`title\`, \`content\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`title\` = ? limit ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`name\` as \`name\`, \`d0\`.\`age\` as \`age\`, \`d0\`.\`email\` as \`email\` from \`users\` as \`d0\` where \`d0\`.\`name\` = ? limit ? + insert into \`posts\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, ?) + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`title\` as \`title\`, \`d0\`.\`content\` as \`content\`, \`d0\`.\`authorId\` as \`authorId\` from \`posts\` as \`d0\` where \`d0\`.\`title\` = ? limit ? " `) }) it("should update post information correctly", async () => { const q = /* GraphQL */ ` - mutation updatePost($set: PostUpdateInput!, $where: PostFilters!) { - updatePost(set: $set, where: $where) { + mutation updatePosts($set: PostUpdateInput!, $where: PostFilters!) { + updatePosts(set: $set, where: $where) { isSuccess } } ` const [PostUID] = await db - .insert(post) + .insert(posts) .values({ title: "Post U" }) .$returningId() - const PostU = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostUID.id), + const PostU = await db.query.posts.findFirst({ + where: { id: PostUID.id }, }) if (!PostU) throw new Error("Post not found") @@ -394,40 +447,40 @@ describe("resolver by mysql", () => { where: { id: { eq: PostU.id } }, }) ).resolves.toMatchObject({ - updatePost: { isSuccess: true }, + updatePosts: { isSuccess: true }, }) // Verify the post was updated - const updatedPost = await db.query.drizzle_post.findFirst({ - where: eq(post.title, "Updated Post U"), + const updatedPost = await db.query.posts.findFirst({ + where: { title: "Updated Post U" }, }) expect(updatedPost).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into \`drizzle_post\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, default) - select \`id\`, \`title\`, \`content\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`id\` = ? limit ? - update \`drizzle_post\` set \`title\` = ? where \`drizzle_post\`.\`id\` = ? - select \`id\`, \`title\`, \`content\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`title\` = ? limit ? + insert into \`posts\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, default) + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`title\` as \`title\`, \`d0\`.\`content\` as \`content\`, \`d0\`.\`authorId\` as \`authorId\` from \`posts\` as \`d0\` where \`d0\`.\`id\` = ? limit ? + update \`posts\` set \`title\` = ? where \`posts\`.\`id\` = ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`title\` as \`title\`, \`d0\`.\`content\` as \`content\`, \`d0\`.\`authorId\` as \`authorId\` from \`posts\` as \`d0\` where \`d0\`.\`title\` = ? limit ? " `) }) it("should delete a post correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromPost($where: PostFilters!) { - deleteFromPost(where: $where) { + mutation deleteFromPosts($where: PostFilters!) { + deleteFromPosts(where: $where) { isSuccess } } ` const [PostDID] = await db - .insert(post) + .insert(posts) .values({ title: "Post D" }) .$returningId() - const PostD = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostDID.id), + const PostD = await db.query.posts.findFirst({ + where: { id: PostDID.id }, }) if (!PostD) throw new Error("Post not found") @@ -436,20 +489,20 @@ describe("resolver by mysql", () => { where: { id: { eq: PostD.id } }, }) ).resolves.toMatchObject({ - deleteFromPost: { isSuccess: true }, + deleteFromPosts: { isSuccess: true }, }) // Verify the post was deleted - const deletedPost = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostD.id), + const deletedPost = await db.query.posts.findFirst({ + where: { id: PostD.id }, }) expect(deletedPost).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into \`drizzle_post\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, default) - select \`id\`, \`title\`, \`content\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`id\` = ? limit ? - delete from \`drizzle_post\` where \`drizzle_post\`.\`id\` = ? - select \`id\`, \`title\`, \`content\`, \`authorId\` from \`drizzle_post\` where \`drizzle_post\`.\`id\` = ? limit ? + insert into \`posts\` (\`id\`, \`title\`, \`content\`, \`authorId\`) values (default, ?, default, default) + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`title\` as \`title\`, \`d0\`.\`content\` as \`content\`, \`d0\`.\`authorId\` as \`authorId\` from \`posts\` as \`d0\` where \`d0\`.\`id\` = ? limit ? + delete from \`posts\` where \`posts\`.\`id\` = ? + select \`d0\`.\`id\` as \`id\`, \`d0\`.\`title\` as \`title\`, \`d0\`.\`content\` as \`content\`, \`d0\`.\`authorId\` as \`authorId\` from \`posts\` as \`d0\` where \`d0\`.\`id\` = ? limit ? " `) }) diff --git a/packages/drizzle/test/resolver-postgres.spec.gql b/packages/drizzle/test/resolver-postgres.spec.gql index a6e51180..159fce5e 100644 --- a/packages/drizzle/test/resolver-postgres.spec.gql +++ b/packages/drizzle/test/resolver-postgres.spec.gql @@ -1,12 +1,12 @@ type Mutation { - deleteFromPost(where: PostFilters): [Post!]! - deleteFromUser(where: UserFilters): [User!]! - insertIntoPost(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, values: [PostInsertInput!]!): [Post!]! - insertIntoPostSingle(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, value: PostInsertInput!): Post - insertIntoUser(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, values: [UserInsertInput!]!): [User!]! - insertIntoUserSingle(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, value: UserInsertInput!): User - updatePost(set: PostUpdateInput!, where: PostFilters): [Post!]! - updateUser(set: UserUpdateInput!, where: UserFilters): [User!]! + deleteFromPosts(where: PostFilters): [Post!]! + deleteFromUsers(where: UserFilters): [User!]! + insertIntoPosts(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, values: [PostInsertInput!]!): [Post!]! + insertIntoPostsSingle(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, value: PostInsertInput!): Post + insertIntoUsers(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, values: [UserInsertInput!]!): [User!]! + insertIntoUsersSingle(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, value: UserInsertInput!): User + updatePosts(set: PostUpdateInput!, where: PostFilters): [Post!]! + updateUsers(set: UserUpdateInput!, where: UserFilters): [User!]! } enum OrderDirection { @@ -15,66 +15,72 @@ enum OrderDirection { } input PgIntegerFilters { - OR: [PgIntegerFiltersOr!] + AND: [PgIntegerFiltersNested!] + NOT: PgIntegerFiltersNested + OR: [PgIntegerFiltersNested!] eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } -input PgIntegerFiltersOr { +input PgIntegerFiltersNested { eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } input PgSerialFilters { - OR: [PgSerialFiltersOr!] + AND: [PgSerialFiltersNested!] + NOT: PgSerialFiltersNested + OR: [PgSerialFiltersNested!] eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } -input PgSerialFiltersOr { +input PgSerialFiltersNested { eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } input PgTextFilters { - OR: [PgTextFiltersOr!] + AND: [PgTextFiltersNested!] + NOT: PgTextFiltersNested + OR: [PgTextFiltersNested!] eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -82,16 +88,16 @@ input PgTextFilters { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } -input PgTextFiltersOr { +input PgTextFiltersNested { eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -99,13 +105,13 @@ input PgTextFiltersOr { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } """A post""" type Post { - author: User + author(where: UserFilters): User authorId: Int content: String id: Int! @@ -116,7 +122,9 @@ type Post { """A post""" input PostFilters { - OR: [PostFiltersOr!] + AND: [PostFiltersNested!] + NOT: PostFiltersNested + OR: [PostFiltersNested!] authorId: PgIntegerFilters content: PgTextFilters id: PgSerialFilters @@ -125,7 +133,7 @@ input PostFilters { title: PgTextFilters } -input PostFiltersOr { +input PostFiltersNested { authorId: PgIntegerFilters content: PgTextFilters id: PgSerialFilters @@ -185,12 +193,12 @@ input PostUpdateInput { } type Query { - post(limit: Int, offset: Int, orderBy: [PostOrderBy!], where: PostFilters): [Post!]! - postCount(where: PostFilters): Int! - postSingle(offset: Int, orderBy: [PostOrderBy!], where: PostFilters): Post - user(limit: Int, offset: Int, orderBy: [UserOrderBy!], where: UserFilters): [User!]! - userCount(where: UserFilters): Int! - userSingle(offset: Int, orderBy: [UserOrderBy!], where: UserFilters): User + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! + postsCount(where: PostFilters): Int! + postsSingle(offset: Int, orderBy: PostOrderBy, where: PostFilters): Post + users(limit: Int, offset: Int, orderBy: UserOrderBy, where: UserFilters): [User!]! + usersCount(where: UserFilters): Int! + usersSingle(offset: Int, orderBy: UserOrderBy, where: UserFilters): User } """A user""" @@ -201,12 +209,14 @@ type User { """The name of the user""" name: String! - posts: [Post!]! + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! } """A user""" input UserFilters { - OR: [UserFiltersOr!] + AND: [UserFiltersNested!] + NOT: UserFiltersNested + OR: [UserFiltersNested!] age: PgIntegerFilters email: PgTextFilters id: PgSerialFilters @@ -215,7 +225,7 @@ input UserFilters { name: PgTextFilters } -input UserFiltersOr { +input UserFiltersNested { age: PgIntegerFilters email: PgTextFilters id: PgSerialFilters diff --git a/packages/drizzle/test/resolver-postgres.spec.ts b/packages/drizzle/test/resolver-postgres.spec.ts index c2d582c7..fcf464fe 100644 --- a/packages/drizzle/test/resolver-postgres.spec.ts +++ b/packages/drizzle/test/resolver-postgres.spec.ts @@ -1,5 +1,5 @@ import { weave } from "@gqloom/core" -import { eq } from "drizzle-orm" +import { ValibotWeaver } from "@gqloom/valibot" import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres" import { type GraphQLSchema, @@ -7,20 +7,17 @@ import { printSchema, } from "graphql" import { type YogaServerInstance, createYoga } from "graphql-yoga" +import * as v from "valibot" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { config } from "../env.config" import { drizzleResolverFactory } from "../src" -import { post, postsRelations, user, usersRelations } from "./schema/postgres" +import { posts, users } from "./schema/postgres" +import { relations } from "./schema/postgres-relations" -const schema = { - drizzle_user: user, - drizzle_post: post, - usersRelations, - postsRelations, -} +const schema = { users, posts } describe("resolver by postgres", () => { - let db: NodePgDatabase + let db: NodePgDatabase let logs: string[] = [] let gqlSchema: GraphQLSchema let yoga: YogaServerInstance<{}, {}> @@ -40,7 +37,7 @@ describe("resolver by postgres", () => { const { data, errors } = await response.json() if (response.status !== 200 || errors != null) { - console.info(errors) + // console.info(errors) throw new Error(JSON.stringify(errors)) } return data @@ -49,32 +46,37 @@ describe("resolver by postgres", () => { beforeAll(async () => { try { db = drizzle(config.postgresUrl, { - schema, + relations, logger: { logQuery: (query) => logs.push(query) }, }) - const userFactory = drizzleResolverFactory(db, "drizzle_user") - const postFactory = drizzleResolverFactory(db, "drizzle_post") + const userFactory = drizzleResolverFactory(db, users, { + input: { + email: v.nullish(v.pipe(v.string(), v.email())), + }, + }) + const postFactory = drizzleResolverFactory(db, posts) gqlSchema = weave( - userFactory.resolver({ name: "user" }), - postFactory.resolver({ name: "post" }) + ValibotWeaver, + userFactory.resolver({ name: "users" }), + postFactory.resolver({ name: "posts" }) ) yoga = createYoga({ schema: gqlSchema }) await db - .insert(user) + .insert(users) .values([{ name: "Tom" }, { name: "Tony" }, { name: "Taylor" }]) - const Tom = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) - const Tony = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) - const Taylor = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Taylor"), + const Taylor = await db.query.users.findFirst({ + where: { name: "Taylor" }, }) if (!Tom || !Tony || !Taylor) throw new Error("User not found") - await db.insert(post).values([ + await db.insert(posts).values([ { title: "Post 1", authorId: Tom.id }, { title: "Post 2", authorId: Tony.id }, { title: "Post 3", authorId: Taylor.id }, @@ -89,8 +91,8 @@ describe("resolver by postgres", () => { logs = [] }) afterAll(async () => { - await db.delete(post) - await db.delete(user) + await db.delete(posts) + await db.delete(users) }) it("should weave GraphQL schema correctly", async () => { @@ -102,45 +104,45 @@ describe("resolver by postgres", () => { describe("query", () => { it("should query users correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy!, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { id name } } - ` + ` await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], + users: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 2, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }], + users: [{ name: "Taylor" }, { name: "Tom" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 1, offset: 1, }) ).resolves.toMatchObject({ - user: [{ name: "Tom" }], + users: [{ name: "Tom" }], }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "drizzle_user" where "drizzle_user"."name" like $1 order by "drizzle_user"."name" asc - select "id", "name" from "drizzle_user" where "drizzle_user"."name" like $1 order by "drizzle_user"."name" asc limit $2 - select "id", "name" from "drizzle_user" where "drizzle_user"."name" like $1 order by "drizzle_user"."name" asc limit $2 offset $3 + select "id", "name" from "users" where "users"."name" like $1 order by "users"."name" asc + select "id", "name" from "users" where "users"."name" like $1 order by "users"."name" asc limit $2 + select "id", "name" from "users" where "users"."name" like $1 order by "users"."name" asc limit $2 offset $3 " `) }) @@ -149,31 +151,31 @@ describe("resolver by postgres", () => { await expect( execute( /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $offset: Int) { - userSingle(orderBy: $orderBy, where: $where, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $offset: Int) { + usersSingle(orderBy: $orderBy, where: $where, offset: $offset) { id name } } - `, + `, { where: { name: { eq: "Taylor" } }, } ) ).resolves.toMatchObject({ - userSingle: { name: "Taylor" }, + usersSingle: { name: "Taylor" }, }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 + select "id", "name" from "users" where "users"."name" = $1 limit $2 " `) }) it("should query user with posts correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { id name posts { @@ -186,11 +188,11 @@ describe("resolver by postgres", () => { await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [ + users: [ { name: "Taylor", posts: [{ title: "Post 3" }], @@ -207,8 +209,8 @@ describe("resolver by postgres", () => { }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "drizzle_user" where "drizzle_user"."name" like $1 order by "drizzle_user"."name" asc - select "id", "title", "authorId" from "drizzle_post" where "drizzle_post"."authorId" in ($1, $2, $3) + select "id", "name" from "users" where "users"."name" like $1 order by "users"."name" asc + select "d0"."id" as "id", "posts"."r" as "posts" from "users" as "d0" left join lateral(select coalesce(json_agg(row_to_json("t".*)), '[]') as "r" from (select "d1"."id" as "id", "d1"."title" as "title" from "posts" as "d1" where "d0"."id" = "d1"."authorId") as "t") as "posts" on true where "d0"."id" in ($1, $2, $3) " `) }) @@ -217,8 +219,8 @@ describe("resolver by postgres", () => { describe("mutation", () => { it("should insert a new user correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUser($values: [UserInsertInput!]!) { - insertIntoUser(values: $values) { + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { id name } @@ -230,26 +232,52 @@ describe("resolver by postgres", () => { values: [{ name: "Tina" }], }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "Tina" }], + insertIntoUsers: [{ name: "Tina" }], }) // Verify the user was inserted - const Tina = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tina"), + const Tina = await db.query.users.findFirst({ + where: { name: "Tina" }, }) expect(Tina).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_user" ("id", "name", "age", "email") values (default, $1, default, default) returning "id", "name" - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 + insert into "users" ("id", "name", "age", "email") values (default, $1, default, default) returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = $1 limit $2 " `) }) + it("should throw error when insert a user with invalid email", async () => { + const q1 = /* GraphQL */ ` + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { + id + name + } + } + ` + await expect( + execute(q1, { values: [{ name: "Tina", email: "modevol.com" }] }) + ).rejects.toThrow("Invalid email") + + const q2 = /* GraphQL */ ` + mutation insertIntoUsersSingle($value: UserInsertInput!) { + insertIntoUsersSingle(value: $value) { + id + name + } + } + ` + await expect( + execute(q2, { value: { name: "Tina", email: "modevol.com" } }) + ).rejects.toThrow("Invalid email") + }) + it("should insert a user with on conflict correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUser($values: [UserInsertInput!]!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { - insertIntoUser(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, values: $values) { + mutation insertIntoUsers($values: [UserInsertInput!]!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { + insertIntoUsers(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, values: $values) { id name } @@ -261,7 +289,7 @@ describe("resolver by postgres", () => { values: [{ name: "Tina", id: 77 }], }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "Tina" }], + insertIntoUsers: [{ name: "Tina" }], }) await expect( @@ -270,7 +298,7 @@ describe("resolver by postgres", () => { doNothing: {}, }) ).resolves.toMatchObject({ - insertIntoUser: [], + insertIntoUsers: [], }) await expect( @@ -279,7 +307,7 @@ describe("resolver by postgres", () => { doNothing: { target: ["id"] }, }) ).resolves.toMatchObject({ - insertIntoUser: [], + insertIntoUsers: [], }) await expect( @@ -291,22 +319,22 @@ describe("resolver by postgres", () => { }, }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "TinaUpdate" }], + insertIntoUsers: [{ name: "TinaUpdate" }], }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict do nothing returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do nothing returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do update set "name" = $3 returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do update set "name" = $3 returning "id", "name" " `) }) it("should insert a single user with on conflict correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUserSingle($value: UserInsertInput!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { - insertIntoUserSingle(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, value: $value) { + mutation insertIntoUsersSingle($value: UserInsertInput!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { + insertIntoUsersSingle(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, value: $value) { id name } @@ -318,7 +346,7 @@ describe("resolver by postgres", () => { value: { name: "Tina", id: 78 }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: { name: "Tina" }, + insertIntoUsersSingle: { name: "Tina" }, }) await expect( @@ -327,7 +355,7 @@ describe("resolver by postgres", () => { doNothing: {}, }) ).resolves.toMatchObject({ - insertIntoUserSingle: null, + insertIntoUsersSingle: null, }) await expect( @@ -336,7 +364,7 @@ describe("resolver by postgres", () => { doNothing: { target: ["id"] }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: null, + insertIntoUsersSingle: null, }) await expect( @@ -348,22 +376,22 @@ describe("resolver by postgres", () => { }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: { name: "TinaUpdate" }, + insertIntoUsersSingle: { name: "TinaUpdate" }, }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict do nothing returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do nothing returning "id", "name" - insert into "drizzle_user" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do update set "name" = $3 returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values ($1, $2, default, default) on conflict ("id") do update set "name" = $3 returning "id", "name" " `) }) it("should update user information correctly", async () => { const q = /* GraphQL */ ` - mutation updateUser($set: UserUpdateInput!, $where: UserFilters!) { - updateUser(set: $set, where: $where) { + mutation updateUsers($set: UserUpdateInput!, $where: UserFilters!) { + updateUsers(set: $set, where: $where) { id name } @@ -371,11 +399,11 @@ describe("resolver by postgres", () => { ` const [TroyID] = await db - .insert(user) + .insert(users) .values({ name: "Troy" }) .returning() - const Troy = await db.query.drizzle_user.findFirst({ - where: eq(user.id, TroyID.id), + const Troy = await db.query.users.findFirst({ + where: { id: TroyID.id }, }) if (!Troy) throw new Error("User not found") @@ -385,36 +413,58 @@ describe("resolver by postgres", () => { where: { id: { eq: Troy.id } }, }) ).resolves.toMatchObject({ - updateUser: [{ id: Troy.id, name: "Tiffany" }], + updateUsers: [{ id: Troy.id, name: "Tiffany" }], }) // Verify the user was updated - const updatedUser = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tiffany"), + const updatedUser = await db.query.users.findFirst({ + where: { name: "Tiffany" }, }) expect(updatedUser).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_user" ("id", "name", "age", "email") values (default, $1, default, default) returning "id", "name", "age", "email" - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."id" = $1 limit $2 - update "drizzle_user" set "name" = $1 where "drizzle_user"."id" = $2 returning "id", "name" - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 + insert into "users" ("id", "name", "age", "email") values (default, $1, default, default) returning "id", "name", "age", "email" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."id" = $1 limit $2 + update "users" set "name" = $1 where "users"."id" = $2 returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = $1 limit $2 " `) }) + it("should throw error when update a user with invalid email", async () => { + const q = /* GraphQL */ ` + mutation updateUsers($set: UserUpdateInput!, $where: UserFilters!) { + updateUsers(set: $set, where: $where) { + id + name + } + } + ` + const [Danny] = await db + .insert(users) + .values({ name: "Danny" }) + .returning() + + await expect( + execute(q, { + set: { email: "modevol.com" }, + where: { id: { eq: Danny.id } }, + }) + ).rejects.toThrow("Invalid email") + }) + it("should delete a user correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromUser($where: UserFilters!) { - deleteFromUser(where: $where) { + mutation deleteFromUsers($where: UserFilters!) { + deleteFromUsers(where: $where) { id name } } ` - const Tony = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) if (!Tony) throw new Error("User not found") @@ -423,27 +473,27 @@ describe("resolver by postgres", () => { where: { id: { eq: Tony.id } }, }) ).resolves.toMatchObject({ - deleteFromUser: [{ id: Tony.id, name: "Tony" }], + deleteFromUsers: [{ id: Tony.id, name: "Tony" }], }) // Verify the user was deleted - const deletedUser = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tony"), + const deletedUser = await db.query.users.findFirst({ + where: { name: "Tony" }, }) expect(deletedUser).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 - delete from "drizzle_user" where "drizzle_user"."id" = $1 returning "id", "name" - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = $1 limit $2 + delete from "users" where "users"."id" = $1 returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = $1 limit $2 " `) }) it("should insert a new post correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoPost($values: [PostInsertInput!]!) { - insertIntoPost(values: $values) { + mutation insertIntoPosts($values: [PostInsertInput!]!) { + insertIntoPosts(values: $values) { id title authorId @@ -451,8 +501,8 @@ describe("resolver by postgres", () => { } ` - const Tom = await db.query.drizzle_user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) if (!Tom) throw new Error("User not found") @@ -461,27 +511,27 @@ describe("resolver by postgres", () => { values: [{ title: "Post 5", authorId: Tom.id }], }) ).resolves.toMatchObject({ - insertIntoPost: [{ title: "Post 5", authorId: Tom.id }], + insertIntoPosts: [{ title: "Post 5", authorId: Tom.id }], }) // Verify the post was inserted - const p = await db.query.drizzle_post.findFirst({ - where: eq(post.title, "Post 5"), + const p = await db.query.posts.findFirst({ + where: { title: "Post 5" }, }) expect(p).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name", "age", "email" from "drizzle_user" where "drizzle_user"."name" = $1 limit $2 - insert into "drizzle_post" ("id", "title", "content", "authorId") values (default, $1, default, $2) returning "id", "title", "authorId" - select "id", "title", "content", "authorId" from "drizzle_post" where "drizzle_post"."title" = $1 limit $2 + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = $1 limit $2 + insert into "posts" ("id", "title", "content", "authorId") values (default, $1, default, $2) returning "id", "title", "authorId" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId" from "posts" as "d0" where "d0"."title" = $1 limit $2 " `) }) it("should update post information correctly", async () => { const q = /* GraphQL */ ` - mutation updatePost($set: PostUpdateInput!, $where: PostFilters!) { - updatePost(set: $set, where: $where) { + mutation updatePosts($set: PostUpdateInput!, $where: PostFilters!) { + updatePosts(set: $set, where: $where) { id title } @@ -489,12 +539,12 @@ describe("resolver by postgres", () => { ` const [PostUID] = await db - .insert(post) + .insert(posts) .values({ title: "Post U" }) .returning() - const PostU = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostUID.id), + const PostU = await db.query.posts.findFirst({ + where: { id: PostUID.id }, }) if (!PostU) throw new Error("Post not found") @@ -504,28 +554,28 @@ describe("resolver by postgres", () => { where: { id: { eq: PostU.id } }, }) ).resolves.toMatchObject({ - updatePost: [{ id: PostU.id, title: "Updated Post U" }], + updatePosts: [{ id: PostU.id, title: "Updated Post U" }], }) // Verify the post was updated - const updatedPost = await db.query.drizzle_post.findFirst({ - where: eq(post.title, "Updated Post U"), + const updatedPost = await db.query.posts.findFirst({ + where: { title: "Updated Post U" }, }) expect(updatedPost).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_post" ("id", "title", "content", "authorId") values (default, $1, default, default) returning "id", "title", "content", "authorId" - select "id", "title", "content", "authorId" from "drizzle_post" where "drizzle_post"."id" = $1 limit $2 - update "drizzle_post" set "title" = $1 where "drizzle_post"."id" = $2 returning "id", "title" - select "id", "title", "content", "authorId" from "drizzle_post" where "drizzle_post"."title" = $1 limit $2 + insert into "posts" ("id", "title", "content", "authorId") values (default, $1, default, default) returning "id", "title", "content", "authorId" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId" from "posts" as "d0" where "d0"."id" = $1 limit $2 + update "posts" set "title" = $1 where "posts"."id" = $2 returning "id", "title" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId" from "posts" as "d0" where "d0"."title" = $1 limit $2 " `) }) it("should delete a post correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromPost($where: PostFilters!) { - deleteFromPost(where: $where) { + mutation deleteFromPosts($where: PostFilters!) { + deleteFromPosts(where: $where) { id title } @@ -533,12 +583,12 @@ describe("resolver by postgres", () => { ` const [PostDID] = await db - .insert(post) + .insert(posts) .values({ title: "Post D" }) .returning() - const PostD = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostDID.id), + const PostD = await db.query.posts.findFirst({ + where: { id: PostDID.id }, }) if (!PostD) throw new Error("Post not found") @@ -547,20 +597,20 @@ describe("resolver by postgres", () => { where: { id: { eq: PostD.id } }, }) ).resolves.toMatchObject({ - deleteFromPost: [{ id: PostD.id, title: "Post D" }], + deleteFromPosts: [{ id: PostD.id, title: "Post D" }], }) // Verify the post was deleted - const deletedPost = await db.query.drizzle_post.findFirst({ - where: eq(post.id, PostD.id), + const deletedPost = await db.query.posts.findFirst({ + where: { id: PostD.id }, }) expect(deletedPost).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "drizzle_post" ("id", "title", "content", "authorId") values (default, $1, default, default) returning "id", "title", "content", "authorId" - select "id", "title", "content", "authorId" from "drizzle_post" where "drizzle_post"."id" = $1 limit $2 - delete from "drizzle_post" where "drizzle_post"."id" = $1 returning "id", "title" - select "id", "title", "content", "authorId" from "drizzle_post" where "drizzle_post"."id" = $1 limit $2 + insert into "posts" ("id", "title", "content", "authorId") values (default, $1, default, default) returning "id", "title", "content", "authorId" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId" from "posts" as "d0" where "d0"."id" = $1 limit $2 + delete from "posts" where "posts"."id" = $1 returning "id", "title" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId" from "posts" as "d0" where "d0"."id" = $1 limit $2 " `) }) diff --git a/packages/drizzle/test/resolver-sqlite.spec.gql b/packages/drizzle/test/resolver-sqlite.spec.gql index 9171aca2..9d85bda4 100644 --- a/packages/drizzle/test/resolver-sqlite.spec.gql +++ b/packages/drizzle/test/resolver-sqlite.spec.gql @@ -1,12 +1,12 @@ type Mutation { - deleteFromPost(where: PostFilters): [PostItem!]! - deleteFromUser(where: UserFilters): [UserItem!]! - insertIntoPost(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, values: [PostInsertInput!]!): [PostItem!]! - insertIntoPostSingle(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, value: PostInsertInput!): PostItem - insertIntoUser(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, values: [UserInsertInput!]!): [UserItem!]! - insertIntoUserSingle(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, value: UserInsertInput!): UserItem - updatePost(set: PostUpdateInput!, where: PostFilters): [PostItem!]! - updateUser(set: UserUpdateInput!, where: UserFilters): [UserItem!]! + deleteFromPosts(where: PostFilters): [Post!]! + deleteFromUsers(where: UserFilters): [User!]! + insertIntoPosts(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, values: [PostInsertInput!]!): [Post!]! + insertIntoPostsSingle(onConflictDoNothing: PostInsertOnConflictDoNothingInput, onConflictDoUpdate: PostInsertOnConflictDoUpdateInput, value: PostInsertInput!): Post + insertIntoUsers(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, values: [UserInsertInput!]!): [User!]! + insertIntoUsersSingle(onConflictDoNothing: UserInsertOnConflictDoNothingInput, onConflictDoUpdate: UserInsertOnConflictDoUpdateInput, value: UserInsertInput!): User + updatePosts(set: PostUpdateInput!, where: PostFilters): [Post!]! + updateUsers(set: UserUpdateInput!, where: UserFilters): [User!]! } enum OrderDirection { @@ -14,18 +14,33 @@ enum OrderDirection { desc } +type Post { + author(where: UserFilters): User + authorId: Int + content: String + id: Int! + reviewer(where: UserFilters): User + reviewerId: Int + starredBy(limit: Int, offset: Int, orderBy: UserStarPostsOrderBy, where: UserStarPostsFilters): [UserStarPostsItem!]! + title: String! +} + input PostFilters { - OR: [PostFiltersOr!] + AND: [PostFiltersNested!] + NOT: PostFiltersNested + OR: [PostFiltersNested!] authorId: SQLiteIntegerFilters content: SQLiteTextFilters id: SQLiteIntegerFilters + reviewerId: SQLiteIntegerFilters title: SQLiteTextFilters } -input PostFiltersOr { +input PostFiltersNested { authorId: SQLiteIntegerFilters content: SQLiteTextFilters id: SQLiteIntegerFilters + reviewerId: SQLiteIntegerFilters title: SQLiteTextFilters } @@ -33,6 +48,7 @@ input PostInsertInput { authorId: Int content: String id: Int + reviewerId: Int title: String! } @@ -48,18 +64,11 @@ input PostInsertOnConflictDoUpdateInput { targetWhere: PostFilters } -type PostItem { - author: UserItem - authorId: Int - content: String - id: Int! - title: String! -} - input PostOrderBy { authorId: OrderDirection content: OrderDirection id: OrderDirection + reviewerId: OrderDirection title: OrderDirection } @@ -67,6 +76,7 @@ enum PostTableColumn { authorId content id + reviewerId title } @@ -74,52 +84,94 @@ input PostUpdateInput { authorId: Int content: String id: Int + reviewerId: Int title: String } type Query { - post(limit: Int, offset: Int, orderBy: [PostOrderBy!], where: PostFilters): [PostItem!]! - postCount(where: PostFilters): Int! - postSingle(offset: Int, orderBy: [PostOrderBy!], where: PostFilters): PostItem - user(limit: Int, offset: Int, orderBy: [UserOrderBy!], where: UserFilters): [UserItem!]! - userCount(where: UserFilters): Int! - userSingle(offset: Int, orderBy: [UserOrderBy!], where: UserFilters): UserItem + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! + postsCount(where: PostFilters): Int! + postsSingle(offset: Int, orderBy: PostOrderBy, where: PostFilters): Post + users(limit: Int, offset: Int, orderBy: UserOrderBy, where: UserFilters): [User!]! + usersCount(where: UserFilters): Int! + usersSingle(offset: Int, orderBy: UserOrderBy, where: UserFilters): User } input SQLiteIntegerFilters { - OR: [SQLiteIntegerFiltersOr!] + AND: [SQLiteIntegerFiltersNested!] + NOT: SQLiteIntegerFiltersNested + OR: [SQLiteIntegerFiltersNested!] eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } -input SQLiteIntegerFiltersOr { +input SQLiteIntegerFiltersNested { eq: Int gt: Int gte: Int - inArray: [Int!] + in: [Int!] isNotNull: Boolean isNull: Boolean lt: Int lte: Int ne: Int - notInArray: [Int!] + notIn: [Int!] } input SQLiteTextFilters { - OR: [SQLiteTextFiltersOr!] + AND: [SQLiteTextFiltersNested!] + NOT: SQLiteTextFiltersNested + OR: [SQLiteTextFiltersNested!] + eq: String + gt: String + gte: String + ilike: String + in: [String!] + isNotNull: Boolean + isNull: Boolean + like: String + lt: String + lte: String + ne: String + notIlike: String + notIn: [String!] + notLike: String +} + +input SQLiteTextFiltersNested { + eq: String + gt: String + gte: String + ilike: String + in: [String!] + isNotNull: Boolean + isNull: Boolean + like: String + lt: String + lte: String + ne: String + notIlike: String + notIn: [String!] + notLike: String +} + +input SQLiteTimestampFilters { + AND: [SQLiteTimestampFiltersNested!] + NOT: SQLiteTimestampFiltersNested + OR: [SQLiteTimestampFiltersNested!] eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -127,16 +179,16 @@ input SQLiteTextFilters { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } -input SQLiteTextFiltersOr { +input SQLiteTimestampFiltersNested { eq: String gt: String gte: String ilike: String - inArray: [String!] + in: [String!] isNotNull: Boolean isNull: Boolean like: String @@ -144,19 +196,59 @@ input SQLiteTextFiltersOr { lte: String ne: String notIlike: String - notInArray: [String!] + notIn: [String!] notLike: String } -type StudentToCourseItem { +input StudentToCoursesFilters { + AND: [StudentToCoursesFiltersNested!] + NOT: StudentToCoursesFiltersNested + OR: [StudentToCoursesFiltersNested!] + courseId: SQLiteIntegerFilters + createdAt: SQLiteTimestampFilters + studentId: SQLiteIntegerFilters +} + +input StudentToCoursesFiltersNested { + courseId: SQLiteIntegerFilters + createdAt: SQLiteTimestampFilters + studentId: SQLiteIntegerFilters +} + +type StudentToCoursesItem { courseId: Int createdAt: String studentId: Int } +input StudentToCoursesOrderBy { + courseId: OrderDirection + createdAt: OrderDirection + studentId: OrderDirection +} + +"""A user""" +type User { + """The age of the user""" + age: Int + courses(limit: Int, offset: Int, orderBy: StudentToCoursesOrderBy, where: StudentToCoursesFilters): [StudentToCoursesItem!]! + + """The email of the user""" + email: String + id: Int! + + """The name of the user""" + name: String! + posts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! + reviewedPosts(limit: Int, offset: Int, orderBy: PostOrderBy, where: PostFilters): [Post!]! + starredPosts(limit: Int, offset: Int, orderBy: UserStarPostsOrderBy, where: UserStarPostsFilters): [UserStarPostsItem!]! +} + """A user""" input UserFilters { - OR: [UserFiltersOr!] + AND: [UserFiltersNested!] + NOT: UserFiltersNested + OR: [UserFiltersNested!] """The age of the user""" age: SQLiteIntegerFilters @@ -169,7 +261,7 @@ input UserFilters { name: SQLiteTextFilters } -input UserFiltersOr { +input UserFiltersNested { """The age of the user""" age: SQLiteIntegerFilters @@ -206,21 +298,6 @@ input UserInsertOnConflictDoUpdateInput { targetWhere: UserFilters } -"""A user""" -type UserItem { - """The age of the user""" - age: Int - courses: [StudentToCourseItem!]! - - """The email of the user""" - email: String - id: Int! - - """The name of the user""" - name: String! - posts: [PostItem!]! -} - input UserOrderBy { """The age of the user""" age: OrderDirection @@ -233,6 +310,29 @@ input UserOrderBy { name: OrderDirection } +input UserStarPostsFilters { + AND: [UserStarPostsFiltersNested!] + NOT: UserStarPostsFiltersNested + OR: [UserStarPostsFiltersNested!] + postId: SQLiteIntegerFilters + userId: SQLiteIntegerFilters +} + +input UserStarPostsFiltersNested { + postId: SQLiteIntegerFilters + userId: SQLiteIntegerFilters +} + +type UserStarPostsItem { + postId: Int + userId: Int +} + +input UserStarPostsOrderBy { + postId: OrderDirection + userId: OrderDirection +} + enum UserTableColumn { """The age of the user""" age diff --git a/packages/drizzle/test/resolver-sqlite.spec.ts b/packages/drizzle/test/resolver-sqlite.spec.ts index 1edf09ef..f24f86e8 100644 --- a/packages/drizzle/test/resolver-sqlite.spec.ts +++ b/packages/drizzle/test/resolver-sqlite.spec.ts @@ -1,5 +1,5 @@ import { weave } from "@gqloom/core" -import { eq } from "drizzle-orm" +import { ValibotWeaver } from "@gqloom/valibot" import { type LibSQLDatabase, drizzle } from "drizzle-orm/libsql" import { type GraphQLSchema, @@ -7,15 +7,17 @@ import { printSchema, } from "graphql" import { type YogaServerInstance, createYoga } from "graphql-yoga" +import * as v from "valibot" import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { drizzleResolverFactory } from "../src" -import * as schema from "./schema/sqlite" -import { post, user } from "./schema/sqlite" +import type * as schema from "./schema/sqlite" +import { posts, users } from "./schema/sqlite" +import { relations } from "./schema/sqlite-relations" const pathToDB = new URL("./schema/sqlite-1.db", import.meta.url) describe("resolver by sqlite", () => { - let db: LibSQLDatabase + let db: LibSQLDatabase let logs: string[] = [] let gqlSchema: GraphQLSchema let yoga: YogaServerInstance<{}, {}> @@ -35,7 +37,7 @@ describe("resolver by sqlite", () => { const { data, errors } = await response.json() if (response.status !== 200 || errors != null) { - console.info(errors) + // console.info(errors) throw new Error(JSON.stringify(errors)) } return data @@ -43,30 +45,38 @@ describe("resolver by sqlite", () => { beforeAll(async () => { db = drizzle({ - schema, + relations, connection: { url: `file:${pathToDB.pathname}` }, logger: { logQuery: (query) => logs.push(query) }, }) - const userFactory = drizzleResolverFactory(db, "user") - const postFactory = drizzleResolverFactory(db, "post") - gqlSchema = weave(userFactory.resolver(), postFactory.resolver()) + const userFactory = drizzleResolverFactory(db, users, { + input: { + email: v.nullish(v.pipe(v.string(), v.email())), + }, + }) + const postFactory = drizzleResolverFactory(db, posts) + gqlSchema = weave( + ValibotWeaver, + userFactory.resolver(), + postFactory.resolver() + ) yoga = createYoga({ schema: gqlSchema }) await db - .insert(user) + .insert(users) .values([{ name: "Tom" }, { name: "Tony" }, { name: "Taylor" }]) - const Tom = await db.query.user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) - const Tony = await db.query.user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) - const Taylor = await db.query.user.findFirst({ - where: eq(user.name, "Taylor"), + const Taylor = await db.query.users.findFirst({ + where: { name: "Taylor" }, }) if (!Tom || !Tony || !Taylor) throw new Error("User not found") - await db.insert(post).values([ + await db.insert(posts).values([ { title: "Post 1", authorId: Tom.id }, { title: "Post 2", authorId: Tony.id }, { title: "Post 3", authorId: Taylor.id }, @@ -79,8 +89,8 @@ describe("resolver by sqlite", () => { }) afterAll(async () => { - await db.delete(post) - await db.delete(user) + await db.delete(posts) + await db.delete(users) }) it("should weave GraphQL schema correctly", async () => { @@ -92,8 +102,8 @@ describe("resolver by sqlite", () => { describe("query", () => { it("should query users correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy, where: $where, limit: $limit, offset: $offset) { id name } @@ -101,36 +111,36 @@ describe("resolver by sqlite", () => { ` await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], + users: [{ name: "Taylor" }, { name: "Tom" }, { name: "Tony" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 2, }) ).resolves.toMatchObject({ - user: [{ name: "Taylor" }, { name: "Tom" }], + users: [{ name: "Taylor" }, { name: "Tom" }], }) await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, limit: 1, offset: 1, }) ).resolves.toMatchObject({ - user: [{ name: "Tom" }], + users: [{ name: "Tom" }], }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "user" where "user"."name" like ? order by "user"."name" asc - select "id", "name" from "user" where "user"."name" like ? order by "user"."name" asc limit ? - select "id", "name" from "user" where "user"."name" like ? order by "user"."name" asc limit ? offset ? + select "id", "name" from "users" where "users"."name" like ? order by "users"."name" asc + select "id", "name" from "users" where "users"."name" like ? order by "users"."name" asc limit ? + select "id", "name" from "users" where "users"."name" like ? order by "users"."name" asc limit ? offset ? " `) }) @@ -139,8 +149,8 @@ describe("resolver by sqlite", () => { await expect( execute( /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $offset: Int) { - userSingle(orderBy: $orderBy, where: $where, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $offset: Int) { + usersSingle(orderBy: $orderBy, where: $where, offset: $offset) { id name } @@ -151,19 +161,19 @@ describe("resolver by sqlite", () => { } ) ).resolves.toMatchObject({ - userSingle: { name: "Taylor" }, + usersSingle: { name: "Taylor" }, }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "user" where "user"."name" = ? limit ? + select "id", "name" from "users" where "users"."name" = ? limit ? " `) }) it("should query user with posts correctly", async () => { const q = /* GraphQL */ ` - query user ($orderBy: [UserOrderBy!], $where: UserFilters!, $limit: Int, $offset: Int) { - user(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { + query users ($orderBy: UserOrderBy, $where: UserFilters!, $limit: Int, $offset: Int) { + users(orderBy: $orderBy,where: $where, limit: $limit, offset: $offset) { id name posts { @@ -176,11 +186,11 @@ describe("resolver by sqlite", () => { await expect( execute(q, { - orderBy: [{ name: "asc" }], + orderBy: { name: "asc" }, where: { name: { like: "T%" } }, }) ).resolves.toMatchObject({ - user: [ + users: [ { name: "Taylor", posts: [{ title: "Post 3" }], @@ -197,8 +207,8 @@ describe("resolver by sqlite", () => { }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name" from "user" where "user"."name" like ? order by "user"."name" asc - select "id", "title", "authorId" from "post" where "post"."authorId" in (?, ?, ?) + select "id", "name" from "users" where "users"."name" like ? order by "users"."name" asc + select "d0"."id" as "id", coalesce((select json_group_array(json_object('id', "id", 'title', "title")) as "r" from (select "d1"."id" as "id", "d1"."title" as "title" from "posts" as "d1" where "d0"."id" = "d1"."authorId") as "t"), jsonb_array()) as "posts" from "users" as "d0" where "d0"."id" in (?, ?, ?) " `) }) @@ -207,8 +217,8 @@ describe("resolver by sqlite", () => { describe("mutation", () => { it("should insert a new user correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUser($values: [UserInsertInput!]!) { - insertIntoUser(values: $values) { + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { id name } @@ -220,26 +230,52 @@ describe("resolver by sqlite", () => { values: [{ name: "Tina" }], }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "Tina" }], + insertIntoUsers: [{ name: "Tina" }], }) // Verify the user was inserted - const Tina = await db.query.user.findFirst({ - where: eq(user.name, "Tina"), + const Tina = await db.query.users.findFirst({ + where: { name: "Tina" }, }) expect(Tina).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "user" ("id", "name", "age", "email") values (null, ?, null, null) returning "id", "name" - select "id", "name", "age", "email" from "user" where "user"."name" = ? limit ? + insert into "users" ("id", "name", "age", "email") values (null, ?, null, null) returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = ? limit ? " `) }) + it("should throw error when insert a user with invalid email", async () => { + const q1 = /* GraphQL */ ` + mutation insertIntoUsers($values: [UserInsertInput!]!) { + insertIntoUsers(values: $values) { + id + name + } + } + ` + await expect( + execute(q1, { values: [{ name: "Tina", email: "modevol.com" }] }) + ).rejects.toThrow("Invalid email") + + const q2 = /* GraphQL */ ` + mutation insertIntoUsersSingle($value: UserInsertInput!) { + insertIntoUsersSingle(value: $value) { + id + name + } + } + ` + await expect( + execute(q2, { value: { name: "Tina", email: "modevol.com" } }) + ).rejects.toThrow("Invalid email") + }) + it("should insert a user with on conflict correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUser($values: [UserInsertInput!]!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { - insertIntoUser(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, values: $values) { + mutation insertIntoUsers($values: [UserInsertInput!]!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { + insertIntoUsers(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, values: $values) { id name } @@ -251,7 +287,7 @@ describe("resolver by sqlite", () => { values: [{ name: "Tina", id: 77 }], }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "Tina" }], + insertIntoUsers: [{ name: "Tina" }], }) await expect( @@ -260,7 +296,7 @@ describe("resolver by sqlite", () => { doNothing: {}, }) ).resolves.toMatchObject({ - insertIntoUser: [], + insertIntoUsers: [], }) await expect( @@ -269,7 +305,7 @@ describe("resolver by sqlite", () => { doNothing: { target: ["id"] }, }) ).resolves.toMatchObject({ - insertIntoUser: [], + insertIntoUsers: [], }) await expect( @@ -281,22 +317,22 @@ describe("resolver by sqlite", () => { }, }) ).resolves.toMatchObject({ - insertIntoUser: [{ name: "TinaUpdate" }], + insertIntoUsers: [{ name: "TinaUpdate" }], }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict do nothing returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("user"."id") do nothing returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("user"."id") do update set "name" = ? returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("users"."id") do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("users"."id") do update set "name" = ? returning "id", "name" " `) }) it("should insert a single user with on conflict correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoUserSingle($value: UserInsertInput!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { - insertIntoUserSingle(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, value: $value) { + mutation insertIntoUsersSingle($value: UserInsertInput!, $doNothing: UserInsertOnConflictDoNothingInput, $doUpdate: UserInsertOnConflictDoUpdateInput) { + insertIntoUsersSingle(onConflictDoNothing: $doNothing, onConflictDoUpdate: $doUpdate, value: $value) { id name } @@ -308,7 +344,7 @@ describe("resolver by sqlite", () => { value: { name: "Tina", id: 78 }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: { name: "Tina" }, + insertIntoUsersSingle: { name: "Tina" }, }) await expect( @@ -317,7 +353,7 @@ describe("resolver by sqlite", () => { doNothing: {}, }) ).resolves.toMatchObject({ - insertIntoUserSingle: null, + insertIntoUsersSingle: null, }) await expect( @@ -326,7 +362,7 @@ describe("resolver by sqlite", () => { doNothing: { target: ["id"] }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: null, + insertIntoUsersSingle: null, }) await expect( @@ -338,29 +374,29 @@ describe("resolver by sqlite", () => { }, }) ).resolves.toMatchObject({ - insertIntoUserSingle: { name: "TinaUpdate" }, + insertIntoUsersSingle: { name: "TinaUpdate" }, }) expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict do nothing returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("user"."id") do nothing returning "id", "name" - insert into "user" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("user"."id") do update set "name" = ? returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("users"."id") do nothing returning "id", "name" + insert into "users" ("id", "name", "age", "email") values (?, ?, null, null) on conflict ("users"."id") do update set "name" = ? returning "id", "name" " `) }) it("should update user information correctly", async () => { const q = /* GraphQL */ ` - mutation updateUser($set: UserUpdateInput!, $where: UserFilters!) { - updateUser(set: $set, where: $where) { + mutation updateUsers($set: UserUpdateInput!, $where: UserFilters!) { + updateUsers(set: $set, where: $where) { id name } } ` - const [Troy] = await db.insert(user).values({ name: "Troy" }).returning() + const [Troy] = await db.insert(users).values({ name: "Troy" }).returning() if (!Troy) throw new Error("User not found") await expect( @@ -369,35 +405,57 @@ describe("resolver by sqlite", () => { where: { id: { eq: Troy.id } }, }) ).resolves.toMatchObject({ - updateUser: [{ id: Troy.id, name: "Tiffany" }], + updateUsers: [{ id: Troy.id, name: "Tiffany" }], }) // Verify the user was updated - const updatedUser = await db.query.user.findFirst({ - where: eq(user.name, "Tiffany"), + const updatedUser = await db.query.users.findFirst({ + where: { name: "Tiffany" }, }) expect(updatedUser).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "user" ("id", "name", "age", "email") values (null, ?, null, null) returning "id", "name", "age", "email" - update "user" set "name" = ? where "user"."id" = ? returning "id", "name" - select "id", "name", "age", "email" from "user" where "user"."name" = ? limit ? + insert into "users" ("id", "name", "age", "email") values (null, ?, null, null) returning "id", "name", "age", "email" + update "users" set "name" = ? where "users"."id" = ? returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = ? limit ? " `) }) + it("should throw error when update a user with invalid email", async () => { + const q = /* GraphQL */ ` + mutation updateUsers($set: UserUpdateInput!, $where: UserFilters!) { + updateUsers(set: $set, where: $where) { + id + name + } + } + ` + const [Danny] = await db + .insert(users) + .values({ name: "Danny" }) + .returning() + + await expect( + execute(q, { + set: { email: "modevol.com" }, + where: { id: { eq: Danny.id } }, + }) + ).rejects.toThrow("Invalid email") + }) + it("should delete a user correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromUser($where: UserFilters!) { - deleteFromUser(where: $where) { + mutation deleteFromUsers($where: UserFilters!) { + deleteFromUsers(where: $where) { id name } } ` - const Tony = await db.query.user.findFirst({ - where: eq(user.name, "Tony"), + const Tony = await db.query.users.findFirst({ + where: { name: "Tony" }, }) if (!Tony) throw new Error("User not found") @@ -406,27 +464,27 @@ describe("resolver by sqlite", () => { where: { id: { eq: Tony.id } }, }) ).resolves.toMatchObject({ - deleteFromUser: [{ id: Tony.id, name: "Tony" }], + deleteFromUsers: [{ id: Tony.id, name: "Tony" }], }) // Verify the user was deleted - const deletedUser = await db.query.user.findFirst({ - where: eq(user.name, "Tony"), + const deletedUser = await db.query.users.findFirst({ + where: { name: "Tony" }, }) expect(deletedUser).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name", "age", "email" from "user" where "user"."name" = ? limit ? - delete from "user" where "user"."id" = ? returning "id", "name" - select "id", "name", "age", "email" from "user" where "user"."name" = ? limit ? + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = ? limit ? + delete from "users" where "users"."id" = ? returning "id", "name" + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = ? limit ? " `) }) it("should insert a new post correctly", async () => { const q = /* GraphQL */ ` - mutation insertIntoPost($values: [PostInsertInput!]!) { - insertIntoPost(values: $values) { + mutation insertIntoPosts($values: [PostInsertInput!]!) { + insertIntoPosts(values: $values) { id title authorId @@ -434,8 +492,8 @@ describe("resolver by sqlite", () => { } ` - const Tom = await db.query.user.findFirst({ - where: eq(user.name, "Tom"), + const Tom = await db.query.users.findFirst({ + where: { name: "Tom" }, }) if (!Tom) throw new Error("User not found") @@ -444,27 +502,27 @@ describe("resolver by sqlite", () => { values: [{ title: "Post 5", authorId: Tom.id }], }) ).resolves.toMatchObject({ - insertIntoPost: [{ title: "Post 5", authorId: Tom.id }], + insertIntoPosts: [{ title: "Post 5", authorId: Tom.id }], }) // Verify the post was inserted - const p = await db.query.post.findFirst({ - where: eq(post.title, "Post 5"), + const p = await db.query.posts.findFirst({ + where: { title: "Post 5" }, }) expect(p).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - select "id", "name", "age", "email" from "user" where "user"."name" = ? limit ? - insert into "post" ("id", "title", "content", "authorId") values (null, ?, null, ?) returning "id", "title", "authorId" - select "id", "title", "content", "authorId" from "post" where "post"."title" = ? limit ? + select "d0"."id" as "id", "d0"."name" as "name", "d0"."age" as "age", "d0"."email" as "email" from "users" as "d0" where "d0"."name" = ? limit ? + insert into "posts" ("id", "title", "content", "authorId", "reviewerId") values (null, ?, null, ?, null) returning "id", "title", "authorId" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId", "d0"."reviewerId" as "reviewerId" from "posts" as "d0" where "d0"."title" = ? limit ? " `) }) it("should update post information correctly", async () => { const q = /* GraphQL */ ` - mutation updatePost($set: PostUpdateInput!, $where: PostFilters!) { - updatePost(set: $set, where: $where) { + mutation updatePosts($set: PostUpdateInput!, $where: PostFilters!) { + updatePosts(set: $set, where: $where) { id title } @@ -472,7 +530,7 @@ describe("resolver by sqlite", () => { ` const [PostU] = await db - .insert(post) + .insert(posts) .values({ title: "Post U" }) .returning() if (!PostU) throw new Error("Post not found") @@ -483,27 +541,27 @@ describe("resolver by sqlite", () => { where: { id: { eq: PostU.id } }, }) ).resolves.toMatchObject({ - updatePost: [{ id: PostU.id, title: "Updated Post U" }], + updatePosts: [{ id: PostU.id, title: "Updated Post U" }], }) // Verify the post was updated - const updatedPost = await db.query.post.findFirst({ - where: eq(post.title, "Updated Post U"), + const updatedPost = await db.query.posts.findFirst({ + where: { title: "Updated Post U" }, }) expect(updatedPost).toBeDefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "post" ("id", "title", "content", "authorId") values (null, ?, null, null) returning "id", "title", "content", "authorId" - update "post" set "title" = ? where "post"."id" = ? returning "id", "title" - select "id", "title", "content", "authorId" from "post" where "post"."title" = ? limit ? + insert into "posts" ("id", "title", "content", "authorId", "reviewerId") values (null, ?, null, null, null) returning "id", "title", "content", "authorId", "reviewerId" + update "posts" set "title" = ? where "posts"."id" = ? returning "id", "title" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId", "d0"."reviewerId" as "reviewerId" from "posts" as "d0" where "d0"."title" = ? limit ? " `) }) it("should delete a post correctly", async () => { const q = /* GraphQL */ ` - mutation deleteFromPost($where: PostFilters!) { - deleteFromPost(where: $where) { + mutation deleteFromPosts($where: PostFilters!) { + deleteFromPosts(where: $where) { id title } @@ -511,7 +569,7 @@ describe("resolver by sqlite", () => { ` const [PostD] = await db - .insert(post) + .insert(posts) .values({ title: "Post D" }) .returning() if (!PostD) throw new Error("Post not found") @@ -521,19 +579,19 @@ describe("resolver by sqlite", () => { where: { id: { eq: PostD.id } }, }) ).resolves.toMatchObject({ - deleteFromPost: [{ id: PostD.id, title: "Post D" }], + deleteFromPosts: [{ id: PostD.id, title: "Post D" }], }) // Verify the post was deleted - const deletedPost = await db.query.post.findFirst({ - where: eq(post.id, PostD.id), + const deletedPost = await db.query.posts.findFirst({ + where: { id: PostD.id }, }) expect(deletedPost).toBeUndefined() expect(["", ...logs, ""].join("\n")).toMatchInlineSnapshot(` " - insert into "post" ("id", "title", "content", "authorId") values (null, ?, null, null) returning "id", "title", "content", "authorId" - delete from "post" where "post"."id" = ? returning "id", "title" - select "id", "title", "content", "authorId" from "post" where "post"."id" = ? limit ? + insert into "posts" ("id", "title", "content", "authorId", "reviewerId") values (null, ?, null, null, null) returning "id", "title", "content", "authorId", "reviewerId" + delete from "posts" where "posts"."id" = ? returning "id", "title" + select "d0"."id" as "id", "d0"."title" as "title", "d0"."content" as "content", "d0"."authorId" as "authorId", "d0"."reviewerId" as "reviewerId" from "posts" as "d0" where "d0"."id" = ? limit ? " `) }) diff --git a/packages/drizzle/test/schema/mysql-relations.ts b/packages/drizzle/test/schema/mysql-relations.ts new file mode 100644 index 00000000..77888663 --- /dev/null +++ b/packages/drizzle/test/schema/mysql-relations.ts @@ -0,0 +1,14 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./mysql" + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, +})) diff --git a/packages/drizzle/test/schema/mysql.ts b/packages/drizzle/test/schema/mysql.ts index 01fa9e78..387baadf 100644 --- a/packages/drizzle/test/schema/mysql.ts +++ b/packages/drizzle/test/schema/mysql.ts @@ -1,13 +1,12 @@ -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/mysql-core" +import { int, mysqlTable, primaryKey, text } from "drizzle-orm/mysql-core" import { drizzleSilk } from "../../src" -export const user = drizzleSilk( - t.mysqlTable("drizzle_user", { - id: t.int().primaryKey().autoincrement(), - name: t.text().notNull(), - age: t.int(), - email: t.text(), +export const users = drizzleSilk( + mysqlTable("users", { + id: int().primaryKey().autoincrement(), + name: text().notNull(), + age: int(), + email: text(), }), { name: "User", @@ -18,16 +17,12 @@ export const user = drizzleSilk( } ) -export const usersRelations = relations(user, ({ many }) => ({ - posts: many(post), -})) - -export const post = drizzleSilk( - t.mysqlTable("drizzle_post", { - id: t.int().primaryKey().autoincrement(), - title: t.text().notNull(), - content: t.text(), - authorId: t.int().references(() => user.id, { onDelete: "cascade" }), +export const posts = drizzleSilk( + mysqlTable("posts", { + id: int().primaryKey().autoincrement(), + title: text().notNull(), + content: text(), + authorId: int().references(() => users.id, { onDelete: "cascade" }), }), { name: "Post", @@ -38,9 +33,11 @@ export const post = drizzleSilk( } ) -export const postsRelations = relations(post, ({ one }) => ({ - author: one(user, { - fields: [post.authorId], - references: [user.id], - }), -})) +export const userStarPosts = mysqlTable( + "userStarPosts", + { + userId: int("user_id").references(() => users.id), + postId: int("post_id").references(() => posts.id), + }, + (t) => [primaryKey({ columns: [t.userId, t.postId] })] +) diff --git a/packages/drizzle/test/schema/postgres-relations.ts b/packages/drizzle/test/schema/postgres-relations.ts new file mode 100644 index 00000000..16383569 --- /dev/null +++ b/packages/drizzle/test/schema/postgres-relations.ts @@ -0,0 +1,14 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./postgres" + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, +})) diff --git a/packages/drizzle/test/schema/postgres.ts b/packages/drizzle/test/schema/postgres.ts index bcc8b4fe..bb10551a 100644 --- a/packages/drizzle/test/schema/postgres.ts +++ b/packages/drizzle/test/schema/postgres.ts @@ -1,13 +1,12 @@ -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/pg-core" +import { integer, pgTable, primaryKey, serial, text } from "drizzle-orm/pg-core" import { drizzleSilk } from "../../src" -export const user = drizzleSilk( - t.pgTable("drizzle_user", { - id: t.serial().primaryKey(), - name: t.text().notNull(), - age: t.integer(), - email: t.text(), +export const users = drizzleSilk( + pgTable("users", { + id: serial().primaryKey(), + name: text().notNull(), + age: integer(), + email: text(), }), { name: "User", @@ -17,16 +16,13 @@ export const user = drizzleSilk( }, } ) -export const usersRelations = relations(user, ({ many }) => ({ - posts: many(post), -})) -export const post = drizzleSilk( - t.pgTable("drizzle_post", { - id: t.serial().primaryKey(), - title: t.text().notNull(), - content: t.text(), - authorId: t.integer().references(() => user.id, { onDelete: "cascade" }), +export const posts = drizzleSilk( + pgTable("posts", { + id: serial().primaryKey(), + title: text().notNull(), + content: text(), + authorId: integer().references(() => users.id, { onDelete: "cascade" }), }), { name: "Post", @@ -36,9 +32,12 @@ export const post = drizzleSilk( }, } ) -export const postsRelations = relations(post, ({ one }) => ({ - author: one(user, { - fields: [post.authorId], - references: [user.id], - }), -})) + +export const userStarPosts = pgTable( + "userStarPosts", + { + userId: integer().references(() => users.id), + postId: integer().references(() => posts.id), + }, + (t) => [primaryKey({ columns: [t.userId, t.postId] })] +) diff --git a/packages/drizzle/test/schema/sqlite-relations.ts b/packages/drizzle/test/schema/sqlite-relations.ts new file mode 100644 index 00000000..d24e56c3 --- /dev/null +++ b/packages/drizzle/test/schema/sqlite-relations.ts @@ -0,0 +1,55 @@ +import { defineRelations } from "drizzle-orm" +import * as schema from "./sqlite" + +export const relations = defineRelations(schema, (r) => ({ + users: { + posts: r.many.posts({ alias: "author" }), + reviewedPosts: r.many.posts({ alias: "reviewer" }), + courses: r.many.studentToCourses(), + starredPosts: r.many.userStarPosts(), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + alias: "author", + }), + reviewer: r.one.users({ + from: r.posts.reviewerId, + to: r.users.id, + alias: "reviewer", + }), + starredBy: r.many.userStarPosts({ + from: r.posts.id, + to: r.userStarPosts.postId, + alias: "starredBy", + }), + }, + courses: { + students: r.many.studentToCourses(), + }, + studentToCourses: { + student: r.one.users({ + from: r.studentToCourses.studentId, + to: r.users.id, + }), + course: r.one.courses({ + from: r.studentToCourses.courseId, + to: r.courses.id, + }), + grade: r.one.studentCourseGrades({ + from: [r.studentToCourses.studentId, r.studentToCourses.courseId], + to: [r.studentCourseGrades.studentId, r.studentCourseGrades.courseId], + }), + }, + userStarPosts: { + user: r.one.users({ + from: r.userStarPosts.userId, + to: r.users.id, + }), + post: r.one.posts({ + from: r.userStarPosts.postId, + to: r.posts.id, + }), + }, +})) diff --git a/packages/drizzle/test/schema/sqlite.ts b/packages/drizzle/test/schema/sqlite.ts index 625fc31e..45c2c0cb 100644 --- a/packages/drizzle/test/schema/sqlite.ts +++ b/packages/drizzle/test/schema/sqlite.ts @@ -1,15 +1,16 @@ -import { relations, sql } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { sql } from "drizzle-orm" +import { int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core" import { drizzleSilk } from "../../src" -export const user = drizzleSilk( - t.sqliteTable("user", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - age: t.int(), - email: t.text(), +export const users = drizzleSilk( + sqliteTable("users", { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + email: text(), }), { + name: "User", description: "A user", fields: { name: { description: "The name of the user" }, @@ -19,68 +20,53 @@ export const user = drizzleSilk( } ) -export const usersRelations = relations(user, ({ many }) => ({ - posts: many(post), - courses: many(studentToCourse), -})) - -export const post = drizzleSilk( - t.sqliteTable("post", { - id: t.int().primaryKey({ autoIncrement: true }), - title: t.text().notNull(), - content: t.text(), - authorId: t.int().references(() => user.id, { onDelete: "cascade" }), - }) -) - -export const postsRelations = relations(post, ({ one }) => ({ - author: one(user, { - fields: [post.authorId], - references: [user.id], +export const posts = drizzleSilk( + sqliteTable("posts", { + id: int().primaryKey({ autoIncrement: true }), + title: text().notNull(), + content: text(), + authorId: int().references(() => users.id, { onDelete: "cascade" }), + reviewerId: int().references(() => users.id, { onDelete: "cascade" }), }), -})) - -export const course = drizzleSilk( - t.sqliteTable("course", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - }) + { name: "Post" } ) -export const coursesRelations = relations(course, ({ many }) => ({ - students: many(studentToCourse), -})) +export const userStarPosts = sqliteTable( + "userStarPosts", + { + userId: int().references(() => users.id), + postId: int().references(() => posts.id), + }, + (t) => [primaryKey({ columns: [t.userId, t.postId] })] +) -export const studentToCourse = drizzleSilk( - t.sqliteTable("studentToCourse", { - studentId: t.int().references(() => user.id), - courseId: t.int().references(() => course.id), - createdAt: t.int({ mode: "timestamp" }).default(sql`(CURRENT_TIMESTAMP)`), +export const courses = drizzleSilk( + sqliteTable("courses", { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), }) ) -export const studentToCourseRelations = relations( - studentToCourse, - ({ one }) => ({ - student: one(user, { - fields: [studentToCourse.studentId], - references: [user.id], - }), - course: one(course, { - fields: [studentToCourse.courseId], - references: [course.id], - }), - grade: one(studentCourseGrade, { - fields: [studentToCourse.studentId, studentToCourse.courseId], - references: [studentCourseGrade.studentId, studentCourseGrade.courseId], - }), - }) +export const studentToCourses = drizzleSilk( + sqliteTable( + "studentToCourses", + { + studentId: int().references(() => users.id), + courseId: int().references(() => courses.id), + createdAt: int({ mode: "timestamp" }).default(sql`(CURRENT_TIMESTAMP)`), + }, + (t) => [primaryKey({ columns: [t.studentId, t.courseId] })] + ) ) -export const studentCourseGrade = drizzleSilk( - t.sqliteTable("studentCourseGrade", { - studentId: t.int().references(() => user.id), - courseId: t.int().references(() => course.id), - grade: t.int(), - }) +export const studentCourseGrades = drizzleSilk( + sqliteTable( + "studentCourseGrades", + { + studentId: int().references(() => users.id), + courseId: int().references(() => courses.id), + grade: int(), + }, + (t) => [primaryKey({ columns: [t.studentId, t.courseId] })] + ) ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71305743..d9084c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,7 +87,7 @@ importers: version: 0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) '@gqloom/drizzle': specifier: latest - version: 0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) + version: 0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) '@gqloom/valibot': specifier: latest version: 0.9.0(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8))(valibot@1.0.0-rc.1(typescript@5.7.3)) @@ -97,12 +97,12 @@ importers: dotenv: specifier: ^16.4.7 version: 16.4.7 - drizzle-orm: - specifier: ^0.39.3 - version: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) graphql: specifier: ^16.8.1 version: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) + graphql-scalars: + specifier: ^1.24.1 + version: 1.24.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) graphql-yoga: specifier: ^5.6.0 version: 5.6.0(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) @@ -114,8 +114,11 @@ importers: specifier: ^22.13.4 version: 22.13.4 drizzle-kit: - specifier: ^0.30.1 - version: 0.30.1 + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562 + drizzle-orm: + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) tsx: specifier: ^4.7.2 version: 4.7.2 @@ -130,7 +133,7 @@ importers: version: 0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) '@gqloom/drizzle': specifier: latest - version: 0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) + version: 0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) '@gqloom/zod': specifier: latest version: 0.9.0(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8))(zod@4.0.0-beta.20250415T232143) @@ -140,9 +143,6 @@ importers: dotenv: specifier: ^16.4.7 version: 16.4.7 - drizzle-orm: - specifier: ^0.39.3 - version: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) graphql: specifier: ^16.8.1 version: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) @@ -157,8 +157,11 @@ importers: specifier: ^22.13.4 version: 22.13.4 drizzle-kit: - specifier: ^0.30.1 - version: 0.30.1 + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562 + drizzle-orm: + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) tsx: specifier: ^4.7.2 version: 4.7.2 @@ -179,7 +182,7 @@ importers: version: 16.4.7 drizzle-seed: specifier: ^0.3.1 - version: 0.3.1(drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7)) + version: 0.3.1(drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7)) graphql: specifier: ^16.8.1 version: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) @@ -197,11 +200,11 @@ importers: specifier: ^8.11.10 version: 8.11.10 drizzle-kit: - specifier: ^0.30.1 - version: 0.30.1 + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562 drizzle-orm: - specifier: ^0.39.3 - version: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) tsx: specifier: ^4.7.2 version: 4.7.2 @@ -246,7 +249,7 @@ importers: version: 6.4.6 '@mikro-orm/postgresql': specifier: ^6.4.6 - version: 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(sqlite3@5.1.7) + version: 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(sqlite3@5.1.7) graphql-yoga: specifier: ^5.6.0 version: 5.6.0(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) @@ -341,11 +344,11 @@ importers: specifier: ^16.4.7 version: 16.4.7 drizzle-kit: - specifier: ^0.30.1 - version: 0.30.1 + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562 drizzle-orm: - specifier: ^0.39.3 - version: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) graphql: specifier: ^16.8.1 version: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) @@ -353,8 +356,8 @@ importers: specifier: ^5.6.0 version: 5.6.0(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) mysql2: - specifier: ^3.12.0 - version: 3.12.0 + specifier: ^3.14.0 + version: 3.14.0 pg: specifier: ^8.13.2 version: 8.13.2 @@ -585,7 +588,7 @@ importers: version: 6.4.6 '@mikro-orm/postgresql': specifier: ^6.4.6 - version: 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(sqlite3@5.1.7) + version: 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(sqlite3@5.1.7) '@tailwindcss/postcss': specifier: ^4.0.3 version: 4.0.3 @@ -605,11 +608,11 @@ importers: specifier: ^16.4.7 version: 16.4.7 drizzle-kit: - specifier: ^0.30.1 - version: 0.30.1 + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562 drizzle-orm: - specifier: ^0.39.3 - version: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) + specifier: 1.0.0-beta.1-7946562 + version: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) fastify: specifier: ^4.28.1 version: 4.28.1 @@ -2066,6 +2069,9 @@ packages: cpu: [x64] os: [win32] + '@petamoriken/float16@3.9.2': + resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3373,12 +3379,12 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - drizzle-kit@0.30.1: - resolution: {integrity: sha512-HmA/NeewvHywhJ2ENXD3KvOuM/+K2dGLJfxVfIHsGwaqKICJnS+Ke2L6UcSrSrtMJLJaT0Im1Qv4TFXfaZShyw==} + drizzle-kit@1.0.0-beta.1-7946562: + resolution: {integrity: sha512-Gkq8UQGKpOjEe2zRDNFknabOFX1y6v+6F3D7fnK2iEdbWcDlDkrRhiitpMVB8+B0K+5Y6j7rBMHtWFdt+YrSug==} hasBin: true - drizzle-orm@0.39.3: - resolution: {integrity: sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==} + drizzle-orm@1.0.0-beta.1-7946562: + resolution: {integrity: sha512-cjMiZZ9Dw6dFsIGmZ2nJNz4vRfEoy2xfxBgSagQKmw27IfNxN4GngENXWdZSP4hDU/+1OHXlS2JxquTw7fJh9Q==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -3396,9 +3402,10 @@ packages: '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' - better-sqlite3: '>=7' + better-sqlite3: '>=9.3.0' bun-types: '*' expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' @@ -3446,6 +3453,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -3519,6 +3528,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -3833,6 +3846,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + gel@2.0.2: + resolution: {integrity: sha512-XTKpfNR9HZOw+k0Bl04nETZjuP5pypVAXsZADSdwr3EtyygTTe1RqvftU2FjGu7Tp9e576a9b/iIOxWrRBxMiQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -4182,6 +4200,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4280,7 +4302,6 @@ packages: libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} - cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] light-my-request@5.13.0: @@ -4763,8 +4784,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mysql2@3.12.0: - resolution: {integrity: sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==} + mysql2@3.14.0: + resolution: {integrity: sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==} engines: {node: '>= 8.0'} mz@2.7.0: @@ -5461,6 +5482,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + shiki@3.4.0: resolution: {integrity: sha512-Ni80XHcqhOEXv5mmDAvf5p6PAJqbUc/RzFeaOqk+zP5DLvTPS3j0ckvA+MI87qoxTQ5RGJDVTbdl/ENLSyyAnQ==} @@ -6088,6 +6113,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -6817,10 +6847,10 @@ snapshots: dependencies: graphql: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) - '@gqloom/drizzle@0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8))': + '@gqloom/drizzle@0.9.3(@gqloom/core@0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7))(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8))': dependencies: '@gqloom/core': 0.9.1(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)) - drizzle-orm: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) + drizzle-orm: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) graphql: 16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8) '@gqloom/mikro-orm@0.7.0(@gqloom/core@0.7.0(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8)))(@mikro-orm/core@6.4.6)(graphql@16.8.1(patch_hash=c3a9767e3d80300c2403f5e7447d2c057178847cbc9a76669fd8e03cb87fc4a8))': @@ -7153,11 +7183,11 @@ snapshots: mikro-orm: 6.4.6 reflect-metadata: 0.2.2 - '@mikro-orm/knex@6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7)': + '@mikro-orm/knex@6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7)': dependencies: '@mikro-orm/core': 6.4.6 fs-extra: 11.3.0 - knex: 3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7) + knex: 3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7) sqlstring: 2.3.3 optionalDependencies: better-sqlite3: 11.8.1 @@ -7174,7 +7204,7 @@ snapshots: '@mikro-orm/libsql@6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)': dependencies: '@mikro-orm/core': 6.4.6 - '@mikro-orm/knex': 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7) fs-extra: 11.3.0 libsql: 0.4.7 sqlstring-sqlite: 0.1.1 @@ -7189,10 +7219,10 @@ snapshots: - supports-color - tedious - '@mikro-orm/postgresql@6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(sqlite3@5.1.7)': + '@mikro-orm/postgresql@6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(sqlite3@5.1.7)': dependencies: '@mikro-orm/core': 6.4.6 - '@mikro-orm/knex': 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.4.6(@mikro-orm/core@6.4.6)(better-sqlite3@11.8.1)(libsql@0.4.7)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7) pg: 8.13.2 postgres-array: 3.0.2 postgres-date: 2.1.0 @@ -7354,6 +7384,8 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.53.0': optional: true + '@petamoriken/float16@3.9.2': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -8648,32 +8680,34 @@ snapshots: dotenv@16.4.7: {} - drizzle-kit@0.30.1: + drizzle-kit@1.0.0-beta.1-7946562: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.19.12 esbuild-register: 3.6.0(esbuild@0.19.12) + gel: 2.0.2 transitivePeerDependencies: - supports-color - drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7): + drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7): optionalDependencies: '@libsql/client': 0.14.0 '@prisma/client': 6.1.0(prisma@6.1.0) '@types/pg': 8.11.10 better-sqlite3: 11.8.1 - knex: 3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7) - mysql2: 3.12.0 + gel: 2.0.2 + knex: 3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7) + mysql2: 3.14.0 pg: 8.13.2 prisma: 6.1.0 sqlite3: 5.1.7 - drizzle-seed@0.3.1(drizzle-orm@0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7)): + drizzle-seed@0.3.1(drizzle-orm@1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7)): dependencies: pure-rand: 6.1.0 optionalDependencies: - drizzle-orm: 0.39.3(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.12.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) + drizzle-orm: 1.0.0-beta.1-7946562(@libsql/client@0.14.0)(@prisma/client@6.1.0(prisma@6.1.0))(@types/pg@8.11.10)(better-sqlite3@11.8.1)(gel@2.0.2)(knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7))(mysql2@3.14.0)(pg@8.13.2)(prisma@6.1.0)(sqlite3@5.1.7) dset@3.1.3: {} @@ -8723,6 +8757,8 @@ snapshots: env-paths@2.2.1: optional: true + env-paths@3.0.0: {} + err-code@2.0.3: optional: true @@ -9282,6 +9318,17 @@ snapshots: wide-align: 1.1.5 optional: true + gel@2.0.2: + dependencies: + '@petamoriken/float16': 3.9.2 + debug: 4.4.0 + env-paths: 3.0.0 + semver: 7.7.1 + shell-quote: 1.8.2 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + generate-function@2.3.1: dependencies: is-property: 1.0.2 @@ -9686,6 +9733,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -9776,7 +9825,7 @@ snapshots: kind-of@6.0.3: {} - knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.12.0)(pg@8.13.2)(sqlite3@5.1.7): + knex@3.1.0(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.2)(sqlite3@5.1.7): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -9794,7 +9843,7 @@ snapshots: tildify: 2.0.0 optionalDependencies: better-sqlite3: 11.8.1 - mysql2: 3.12.0 + mysql2: 3.14.0 pg: 8.13.2 sqlite3: 5.1.7 transitivePeerDependencies: @@ -10566,7 +10615,7 @@ snapshots: ms@2.1.3: {} - mysql2@3.12.0: + mysql2@3.14.0: dependencies: aws-ssl-profiles: 1.1.2 denque: 2.1.0 @@ -11279,8 +11328,7 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.7.1: - optional: true + semver@7.7.1: {} send@0.18.0: dependencies: @@ -11366,6 +11414,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.2: {} + shiki@3.4.0: dependencies: '@shikijs/core': 3.4.0 @@ -11989,6 +12039,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/website/content/docs/dataloader.mdx b/website/content/docs/dataloader.mdx index 742ed265..bfe679ed 100644 --- a/website/content/docs/dataloader.mdx +++ b/website/content/docs/dataloader.mdx @@ -14,9 +14,9 @@ This leads to the well-known N+1 query problem. To solve this problem, we can us Consider that we have two tables, `users` and `posts`, where `posts` is associated with the `id` of `users` through `posts.authorId`: -```ts twoslash +```ts twoslash tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" +import { defineRelations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -31,9 +31,39 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) +export const posts = drizzleSilk( + t.pgTable("posts", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + updatedAt: t + .timestamp() + .defaultNow() + .$onUpdateFn(() => new Date()), + published: t.boolean().default(false), + title: t.varchar({ length: 255 }).notNull(), + authorId: t.integer().notNull(), + }) +) +``` + +```ts twoslash tab="relations.ts" +// @filename: schema.ts + +import { drizzleSilk } from "@gqloom/drizzle" +import { defineRelations } from "drizzle-orm" +import * as t from "drizzle-orm/pg-core" + +export const roleEnum = t.pgEnum("role", ["user", "admin"]) + +export const users = drizzleSilk( + t.pgTable("users", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + email: t.text().unique().notNull(), + name: t.text(), + role: roleEnum().default("user"), + }) +) export const posts = drizzleSilk( t.pgTable("posts", { @@ -48,9 +78,24 @@ export const posts = drizzleSilk( authorId: t.integer().notNull(), }) ) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) ``` @@ -61,7 +106,7 @@ Let's use [drizzle-seed](https://orm.drizzle.team/docs/seed-overview) to populat ```ts import { drizzle } from "drizzle-orm/node-postgres" import { reset, seed } from "drizzle-seed" -import { config } from "../env.config" +import { config } from "./env.config" import * as schema from "./schema" async function main() { @@ -91,7 +136,6 @@ Let's write a simple resolver for the `User` object: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -106,10 +150,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), @@ -124,8 +164,23 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: env.config.ts export const config = { databaseUrl: "" } @@ -135,10 +190,15 @@ import { field, query, resolver, weave } from "@gqloom/core" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/node-postgres" import { config } from "./env.config" +import { relations } from "./relations" import * as tables from "./schema" import { posts, users } from "./schema" -const db = drizzle(config.databaseUrl, { schema: tables, logger: true }) +const db = drizzle(config.databaseUrl, { + schema: tables, + relations, + logger: true, +}) const userResolver = resolver.of(users, { users: query(users.$list()).resolve(() => db.select().from(users)), @@ -179,7 +239,6 @@ Next, we will use DataLoader to optimize our query. ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -194,10 +253,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), @@ -212,9 +267,27 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), -})) +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations( + { users: tables.users, posts: tables.posts }, + (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + }) +) // @filename: env.config.ts export const config = { databaseUrl: "" } // @filename: index.ts @@ -223,10 +296,15 @@ import { field, query, resolver, weave } from "@gqloom/core" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/node-postgres" import { config } from "./env.config" +import { relations } from "./relations" import * as tables from "./schema" import { posts, users } from "./schema" -const db = drizzle(config.databaseUrl, { schema: tables, logger: true }) +const db = drizzle(config.databaseUrl, { + schema: tables, + relations, + logger: true, +}) const userResolver = resolver.of(users, { users: query(users.$list()).resolve(() => db.select().from(users)), diff --git a/website/content/docs/dataloader.zh.mdx b/website/content/docs/dataloader.zh.mdx index 0d9efd52..7d293a3c 100644 --- a/website/content/docs/dataloader.zh.mdx +++ b/website/content/docs/dataloader.zh.mdx @@ -14,9 +14,9 @@ icon: HardDriveDownload 考虑我们有两张表 `users` 和 `posts`,其中 `posts` 通过 `posts.authorId` 关联到 `users` 的 `id`: -```ts twoslash +```ts twoslash tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" +import { defineRelations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -31,9 +31,39 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) +export const posts = drizzleSilk( + t.pgTable("posts", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + updatedAt: t + .timestamp() + .defaultNow() + .$onUpdateFn(() => new Date()), + published: t.boolean().default(false), + title: t.varchar({ length: 255 }).notNull(), + authorId: t.integer().notNull(), + }) +) +``` + +```ts twoslash tab="relations.ts" +// @filename: schema.ts + +import { drizzleSilk } from "@gqloom/drizzle" +import { defineRelations } from "drizzle-orm" +import * as t from "drizzle-orm/pg-core" + +export const roleEnum = t.pgEnum("role", ["user", "admin"]) + +export const users = drizzleSilk( + t.pgTable("users", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + email: t.text().unique().notNull(), + name: t.text(), + role: roleEnum().default("user"), + }) +) export const posts = drizzleSilk( t.pgTable("posts", { @@ -48,9 +78,24 @@ export const posts = drizzleSilk( authorId: t.integer().notNull(), }) ) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) ``` @@ -61,7 +106,7 @@ export const postsRelations = relations(posts, ({ one }) => ({ ```ts import { drizzle } from "drizzle-orm/node-postgres" import { reset, seed } from "drizzle-seed" -import { config } from "../env.config" +import { config } from "./env.config" import * as schema from "./schema" async function main() { @@ -91,7 +136,6 @@ main() ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -106,10 +150,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), @@ -124,8 +164,23 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: env.config.ts export const config = { databaseUrl: "" } @@ -135,10 +190,15 @@ import { field, query, resolver, weave } from "@gqloom/core" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/node-postgres" import { config } from "./env.config" +import { relations } from "./relations" import * as tables from "./schema" import { posts, users } from "./schema" -const db = drizzle(config.databaseUrl, { schema: tables, logger: true }) +const db = drizzle(config.databaseUrl, { + schema: tables, + relations, + logger: true, +}) const userResolver = resolver.of(users, { users: query(users.$list()).resolve(() => db.select().from(users)), @@ -179,7 +239,6 @@ query usersWithPosts { ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -194,10 +253,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.pgTable("posts", { id: t.serial().primaryKey(), @@ -212,9 +267,27 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), -})) +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations( + { users: tables.users, posts: tables.posts }, + (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, + }) +) // @filename: env.config.ts export const config = { databaseUrl: "" } // @filename: index.ts @@ -223,10 +296,15 @@ import { field, query, resolver, weave } from "@gqloom/core" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/node-postgres" import { config } from "./env.config" +import { relations } from "./relations" import * as tables from "./schema" import { posts, users } from "./schema" -const db = drizzle(config.databaseUrl, { schema: tables, logger: true }) +const db = drizzle(config.databaseUrl, { + schema: tables, + relations, + logger: true, +}) const userResolver = resolver.of(users, { users: query(users.$list()).resolve(() => db.select().from(users)), diff --git a/website/content/docs/getting-started.mdx b/website/content/docs/getting-started.mdx index bf14347c..dda7a83f 100644 --- a/website/content/docs/getting-started.mdx +++ b/website/content/docs/getting-started.mdx @@ -44,6 +44,7 @@ Our application will have the following structure: + @@ -324,40 +325,68 @@ yarn add -D drizzle-kit Next, define the database tables in the `src/schema/index.ts` file. We will define two tables, `users` and `cats`, and establish the relationship between them: -```ts twoslash title="src/schema/index.ts" +```ts twoslash title="src/schema/index.ts" tab="src/schema/index.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) +export const cats = drizzleSilk( + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() + .notNull() + .references(() => users.id), + }) +) +``` + +```ts twoslash title="src/schema/relations.ts" tab="src/schema/relations.ts" +// @filename: index.ts +import { drizzleSilk } from "@gqloom/drizzle" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" + +export const users = drizzleSilk( + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), + }) +) export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) ``` @@ -386,12 +415,15 @@ npx drizzle-kit push ### Use the Database To use the database in the application, we need to create a database instance: + ```ts title="src/providers/index.ts" import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) ``` @@ -411,47 +443,51 @@ After completing the user resolver, we also need to add it to the `resolvers` in ```ts twoslash title="src/resolvers/user.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/user.ts @@ -465,19 +501,11 @@ import { users } from "../schema" export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -497,52 +525,55 @@ export const userResolver = resolver.of(users, { ```ts twoslash title="src/resolvers/index.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) -// ---cut--- // @filename: resolvers/user.ts +// ---cut--- import { mutation, query, resolver } from "@gqloom/core" -import { eq } from "drizzle-orm" import * as z from "zod" import { db } from "../providers" import { users } from "../schema" @@ -550,19 +581,11 @@ import { users } from "../schema" export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -691,47 +714,51 @@ Next, let's try to add a simple login function and add a query operation to the To implement this query, we first need to have a login function. Let's write a simple one: ```ts twoslash title="src/contexts/index.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts // ---cut--- @@ -740,14 +767,14 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) + if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -779,19 +806,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -823,19 +842,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -926,49 +937,54 @@ We use the [resolver factory](./schema/drizzle#resolver-factory) to quickly crea ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/cat.ts +// ---cut--- import { field, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import * as v from "valibot" @@ -997,47 +1013,51 @@ export const catResolver = resolver.of(cats, { ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/cat.ts // ---cut--- @@ -1094,47 +1114,51 @@ Next, let's try to add a `createCat` mutation. We want only logged-in users to a ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts import { createMemoization, useContext } from "@gqloom/core/context" @@ -1142,14 +1166,13 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -1207,47 +1230,51 @@ export const catResolver = resolver.of(cats, { ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts import { createMemoization, useContext } from "@gqloom/core/context" @@ -1255,14 +1282,13 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -1521,19 +1547,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -1568,19 +1586,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ diff --git a/website/content/docs/getting-started.zh.mdx b/website/content/docs/getting-started.zh.mdx index e4dadfa8..1e2c86e5 100644 --- a/website/content/docs/getting-started.zh.mdx +++ b/website/content/docs/getting-started.zh.mdx @@ -44,6 +44,7 @@ import { File, Folder, Files } from 'fumadocs-ui/components/files'; + @@ -324,40 +325,68 @@ yarn add -D drizzle-kit 接下来在 `src/schema/index.ts` 文件中定义数据库表格,我们将定义 `users` 和 `cats` 两个表格,并建立它们之间的关系: -```ts twoslash title="src/schema/index.ts" +```ts twoslash title="src/schema/index.ts" tab="src/schema/index.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) +export const cats = drizzleSilk( + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() + .notNull() + .references(() => users.id), + }) +) +``` + +```ts twoslash title="src/schema/relations.ts" tab="src/schema/relations.ts" +// @filename: index.ts +import { drizzleSilk } from "@gqloom/drizzle" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" + +export const users = drizzleSilk( + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), + }) +) export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) ``` @@ -386,12 +415,15 @@ npx drizzle-kit push ### 使用数据库 为了在应用中使用数据库,我们需要创建一个数据库实例: + ```ts title="src/providers/index.ts" import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) ``` @@ -411,47 +443,51 @@ export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { ```ts twoslash title="src/resolvers/user.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/user.ts @@ -465,19 +501,11 @@ import { users } from "../schema" export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -497,52 +525,55 @@ export const userResolver = resolver.of(users, { ```ts twoslash title="src/resolvers/index.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) -// ---cut--- // @filename: resolvers/user.ts +// ---cut--- import { mutation, query, resolver } from "@gqloom/core" -import { eq } from "drizzle-orm" import * as z from "zod" import { db } from "../providers" import { users } from "../schema" @@ -550,19 +581,11 @@ import { users } from "../schema" export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -691,47 +714,51 @@ createServer(yoga).listen(4000, () => { 为了实现这个查询,首先得有登录功能,让我们来简单写一个: ```ts twoslash title="src/contexts/index.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts // ---cut--- @@ -740,14 +767,14 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) + if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -779,19 +806,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -823,19 +842,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -926,49 +937,54 @@ export const userResolver = resolver.of(users, { ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/cat.ts +// ---cut--- import { field, resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import * as v from "valibot" @@ -997,47 +1013,51 @@ export const catResolver = resolver.of(cats, { ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: resolvers/cat.ts // ---cut--- @@ -1094,47 +1114,51 @@ export const resolvers = [helloResolver, userResolver, catResolver] // [!code ++ ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts import { createMemoization, useContext } from "@gqloom/core/context" @@ -1142,14 +1166,13 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -1207,47 +1230,51 @@ export const catResolver = resolver.of(cats, { ```ts twoslash title="src/resolvers/cat.ts" -// @filename: schema.ts +// @filename: schema/index.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" -import * as t from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const users = drizzleSilk( - t.sqliteTable("users", { - id: t.int().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - phone: t.text().notNull().unique(), + sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + phone: text().notNull().unique(), }) ) -export const usersRelations = relations(users, ({ many }) => ({ - cats: many(cats), -})) - export const cats = drizzleSilk( - t.sqliteTable("cats", { - id: t.integer().primaryKey({ autoIncrement: true }), - name: t.text().notNull(), - birthday: t.integer({ mode: "timestamp" }).notNull(), - ownerId: t - .integer() + sqliteTable("cats", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + birthday: integer({ mode: "timestamp" }).notNull(), + ownerId: integer() .notNull() .references(() => users.id), }) ) +// @filename: schema/relations.ts +import { defineRelations } from "drizzle-orm" +import * as schema from "./index" -export const catsRelations = relations(cats, ({ one }) => ({ - owner: one(users, { - fields: [cats.ownerId], - references: [users.id], - }), +export const relations = defineRelations(schema, (r) => ({ + users: { + cats: r.many.cats(), + }, + cats: { + owner: r.one.users({ + from: r.cats.ownerId, + to: r.users.id, + }), + }, })) // @filename: providers/index.ts import { drizzle } from "drizzle-orm/libsql" import * as schema from "../schema" +import { relations } from "../schema/relations" export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", { schema, + relations, }) // @filename: contexts/index.ts import { createMemoization, useContext } from "@gqloom/core/context" @@ -1255,14 +1282,13 @@ import { eq } from "drizzle-orm" import { GraphQLError } from "graphql" import type { YogaInitialContext } from "graphql-yoga" import { db } from "../providers" -import { users } from "../schema" export const useCurrentUser = createMemoization(async () => { const phone = useContext().request.headers.get("authorization") if (phone == null) throw new GraphQLError("Unauthorized") - const user = await db.query.users.findFirst({ where: eq(users.phone, phone) }) + const user = await db.query.users.findFirst({ where: { phone } }) if (user == null) throw new GraphQLError("Unauthorized") return user }) @@ -1521,19 +1547,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: v.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: v.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ @@ -1568,19 +1586,11 @@ export const userResolver = resolver.of(users, { usersByName: query(users.$list()) .input({ name: z.string() }) - .resolve(({ name }) => { - return db.query.users.findMany({ - where: eq(users.name, name), - }) - }), + .resolve(({ name }) => db.query.users.findMany({ where: { name } })), userByPhone: query(users.$nullable()) .input({ phone: z.string() }) - .resolve(({ phone }) => { - return db.query.users.findFirst({ - where: eq(users.phone, phone), - }) - }), + .resolve(({ phone }) => db.query.users.findFirst({ where: { phone } })), createUser: mutation(users) .input({ diff --git a/website/content/docs/schema/drizzle.mdx b/website/content/docs/schema/drizzle.mdx index d7514935..812803fb 100644 --- a/website/content/docs/schema/drizzle.mdx +++ b/website/content/docs/schema/drizzle.mdx @@ -56,9 +56,8 @@ bun add @gqloom/core @gqloom/drizzle We can easily use Drizzle Schemas as [Silk](../silk) by simply wrapping them with `drizzleSilk`. -```ts twoslash title="schema.ts" +```ts twoslash title="schema.ts" tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -71,10 +70,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -83,12 +78,49 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) +``` -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +```ts twoslash title="relations.ts" tab="relations.ts" +// @filename: schema.ts +import { drizzleSilk } from "@gqloom/drizzle" +import * as t from "drizzle-orm/sqlite-core" + +export const users = drizzleSilk( + t.sqliteTable("users", { + id: t.int().primaryKey({ autoIncrement: true }), + name: t.text().notNull(), + age: t.int(), + email: t.text(), + password: t.text(), + }) +) + +export const posts = drizzleSilk( + t.sqliteTable("posts", { + id: t.int().primaryKey({ autoIncrement: true }), + title: t.text().notNull(), + content: t.text(), + authorId: t.int().references(() => users.id, { onDelete: "cascade" }), + }) +) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) ``` @@ -97,7 +129,6 @@ Let's use them in the resolver. At the same time, we use the `useSelectedColumns ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -110,10 +141,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -122,12 +149,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts // ---cut--- @@ -136,11 +174,13 @@ import { useSelectedColumns } from "@gqloom/drizzle/context" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { posts, users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -294,7 +334,6 @@ const usersResolverFactory = drizzleResolverFactory(db, users) ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -307,10 +346,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -319,22 +354,35 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts // ---cut--- import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -350,7 +398,6 @@ In Drizzle Table, we can easily create [relationships](https://orm.drizzle.team/ ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -363,10 +410,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -375,12 +418,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { field, EasyDataLoader } from "@gqloom/core" @@ -392,11 +446,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -463,7 +519,6 @@ We can use the queries from the resolver factory in the resolver: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -476,10 +531,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -488,12 +539,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -501,11 +563,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -545,7 +609,6 @@ We can use the mutations from the resolver factory in the resolver: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -558,10 +621,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -570,23 +629,36 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -612,7 +684,6 @@ The pre-defined queries and mutations of the resolver factory support custom inp ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -625,10 +696,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -637,12 +704,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -650,11 +728,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -674,7 +754,7 @@ export const usersResolver = resolver.of(users, { }) ``` -In the above code, we use `valibot` to define the input type. `v.object({ id: v.number() })` defines the type of the input object, and `v.transform(({ id }) => ({ where: eq(users.id, id) }))` converts the input parameters into Drizzle query parameters. +In the above code, we use `valibot` to define the input type. `v.object({ id: v.number() })` defines the type of the input object, and `v.transform(({ id }) => ({ where: { id } }))` converts the input parameters into Drizzle query parameters. ### Adding Middleware @@ -683,7 +763,6 @@ The pre-defined queries, mutations, and fields of the resolver factory support a ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -696,10 +775,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -708,12 +783,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, field, resolver } from "@gqloom/core" @@ -723,11 +809,13 @@ import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import { GraphQLError } from "graphql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users, posts } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -761,7 +849,6 @@ We can directly create a complete Resolver with the resolver factory: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -774,10 +861,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -786,12 +869,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -799,11 +893,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) diff --git a/website/content/docs/schema/drizzle.zh.mdx b/website/content/docs/schema/drizzle.zh.mdx index f093b3ed..1b86df7f 100644 --- a/website/content/docs/schema/drizzle.zh.mdx +++ b/website/content/docs/schema/drizzle.zh.mdx @@ -56,9 +56,8 @@ bun add @gqloom/core @gqloom/drizzle 只需要使用 `drizzleSilk` 包裹 Drizzle Table,我们就可以轻松地将它们作为[丝线](../silk)使用。 -```ts twoslash title="schema.ts" +```ts twoslash title="schema.ts" tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -71,10 +70,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -83,12 +78,49 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) +``` -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +```ts twoslash title="relations.ts" tab="relations.ts" +// @filename: schema.ts +import { drizzleSilk } from "@gqloom/drizzle" +import * as t from "drizzle-orm/sqlite-core" + +export const users = drizzleSilk( + t.sqliteTable("users", { + id: t.int().primaryKey({ autoIncrement: true }), + name: t.text().notNull(), + age: t.int(), + email: t.text(), + password: t.text(), + }) +) + +export const posts = drizzleSilk( + t.sqliteTable("posts", { + id: t.int().primaryKey({ autoIncrement: true }), + title: t.text().notNull(), + content: t.text(), + authorId: t.int().references(() => users.id, { onDelete: "cascade" }), + }) +) +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) ``` @@ -97,7 +129,6 @@ export const postsRelations = relations(posts, ({ one }) => ({ ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -110,10 +141,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -122,12 +149,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts // ---cut--- @@ -136,11 +174,13 @@ import { useSelectedColumns } from "@gqloom/drizzle/context" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { posts, users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -294,7 +334,6 @@ const usersResolverFactory = drizzleResolverFactory(db, users) ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -307,10 +346,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -319,22 +354,35 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts // ---cut--- import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -350,7 +398,6 @@ const usersResolverFactory = drizzleResolverFactory(db, users) ```ts twoslash title="resolver.ts" // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -363,10 +410,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -375,12 +418,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { field, EasyDataLoader } from "@gqloom/core" @@ -392,11 +446,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq, inArray } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -463,7 +519,6 @@ Drizzle 解析器工厂预置了一些常用的查询: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -476,10 +531,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -488,12 +539,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -501,11 +563,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -545,7 +609,6 @@ Drizzle 解析器工厂预置了一些常用的变更: ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -558,10 +621,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -570,23 +629,36 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { resolver } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -612,7 +684,6 @@ export const usersResolver = resolver.of(users, { ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -625,10 +696,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -637,12 +704,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -650,11 +728,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -674,7 +754,7 @@ export const usersResolver = resolver.of(users, { }) ``` -在上面的代码中,我们使用 `valibot` 来定义输入类型, `v.object({ id: v.number() })` 定义了输入对象的类型,`v.transform(({ id }) => ({ where: eq(users.id, id) }))` 将输入参数转换为 Drizzle 的查询参数。 +在上面的代码中,我们使用 `valibot` 来定义输入类型, `v.object({ id: v.number() })` 定义了输入对象的类型,`v.transform(({ id }) => ({ where: { id } }))` 将输入参数转换为 Drizzle 的查询参数。 ### 添加中间件 @@ -683,7 +763,6 @@ export const usersResolver = resolver.of(users, { ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -696,10 +775,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -708,12 +783,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, field, resolver } from "@gqloom/core" @@ -723,11 +809,13 @@ import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import { GraphQLError } from "graphql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users, posts } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) @@ -761,7 +849,6 @@ const postResolver = resolver.of(posts, { ```ts twoslash // @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/sqlite-core" export const users = drizzleSilk( @@ -774,10 +861,6 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) - export const posts = drizzleSilk( t.sqliteTable("posts", { id: t.int().primaryKey({ autoIncrement: true }), @@ -786,12 +869,23 @@ export const posts = drizzleSilk( authorId: t.int().references(() => users.id, { onDelete: "cascade" }), }) ) - -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { - fields: [posts.authorId], - references: [users.id], - }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ + from: r.users.id, + to: r.posts.authorId, + }), + }, + posts: { + author: r.one.users({ + from: r.posts.authorId, + to: r.users.id, + }), + }, })) // @filename: resolver.ts import { query, resolver } from "@gqloom/core" @@ -799,11 +893,13 @@ import { drizzleResolverFactory } from "@gqloom/drizzle" import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/libsql" import * as v from "valibot" +import { relations } from "./relations" import * as schema from "./schema" import { users } from "./schema" const db = drizzle({ schema, + relations, connection: { url: process.env.DB_FILE_NAME! }, }) diff --git a/website/content/home/drizzle.md b/website/content/home/drizzle.md index 94141e8b..7eb85c82 100644 --- a/website/content/home/drizzle.md +++ b/website/content/home/drizzle.md @@ -1,7 +1,5 @@ ```ts twoslash title="src/index.ts" tab="index.ts" -// @filename: schema.ts import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -16,9 +14,37 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) +export const posts = drizzleSilk( + t.pgTable("posts", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + updatedAt: t + .timestamp() + .defaultNow() + .$onUpdateFn(() => new Date()), + published: t.boolean().default(false), + title: t.varchar({ length: 255 }).notNull(), + authorId: t.integer(), + }) +) +``` + +```ts twoslash title="src/relations.ts" tab="relations.ts" +// @filename: schema.ts +import { drizzleSilk } from "@gqloom/drizzle" +import * as t from "drizzle-orm/pg-core" + +export const roleEnum = t.pgEnum("role", ["user", "admin"]) + +export const users = drizzleSilk( + t.pgTable("users", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + email: t.text().unique().notNull(), + name: t.text(), + role: roleEnum().default("user"), + }) +) export const posts = drizzleSilk( t.pgTable("posts", { @@ -34,8 +60,17 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +// @filename: relations.ts +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ from: r.users.id, to: r.posts.authorId }), + }, + posts: { + author: r.one.users({ from: r.posts.authorId, to: r.users.id }), + }, })) // @filename: index.ts // ---cut--- @@ -44,9 +79,10 @@ import { weave } from "@gqloom/core" import { drizzleResolverFactory } from "@gqloom/drizzle" import { drizzle } from "drizzle-orm/node-postgres" import { createYoga } from "graphql-yoga" +import { relations } from "./relations" import * as tables from "./schema" -const db = drizzle(process.env.DATABASE_URL!, { schema: tables }) +const db = drizzle(process.env.DATABASE_URL!, { schema: tables, relations }) const userResolver = drizzleResolverFactory(db, "users").resolver() const postResolver = drizzleResolverFactory(db, "posts").resolver() @@ -62,7 +98,6 @@ server.listen(4000, () => { ```ts twoslash title="src/schema.ts" tab="schema.ts" import { drizzleSilk } from "@gqloom/drizzle" -import { relations } from "drizzle-orm" import * as t from "drizzle-orm/pg-core" export const roleEnum = t.pgEnum("role", ["user", "admin"]) @@ -77,9 +112,37 @@ export const users = drizzleSilk( }) ) -export const usersRelations = relations(users, ({ many }) => ({ - posts: many(posts), -})) +export const posts = drizzleSilk( + t.pgTable("posts", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + updatedAt: t + .timestamp() + .defaultNow() + .$onUpdateFn(() => new Date()), + published: t.boolean().default(false), + title: t.varchar({ length: 255 }).notNull(), + authorId: t.integer(), + }) +) +``` + +```ts twoslash title="src/relations.ts" tab="relations.ts" +// @filename: schema.ts +import { drizzleSilk } from "@gqloom/drizzle" +import * as t from "drizzle-orm/pg-core" + +export const roleEnum = t.pgEnum("role", ["user", "admin"]) + +export const users = drizzleSilk( + t.pgTable("users", { + id: t.serial().primaryKey(), + createdAt: t.timestamp().defaultNow(), + email: t.text().unique().notNull(), + name: t.text(), + role: roleEnum().default("user"), + }) +) export const posts = drizzleSilk( t.pgTable("posts", { @@ -95,8 +158,18 @@ export const posts = drizzleSilk( }) ) -export const postsRelations = relations(posts, ({ one }) => ({ - author: one(users, { fields: [posts.authorId], references: [users.id] }), +// @filename: relations.ts +// ---cut--- +import { defineRelations } from "drizzle-orm" +import * as tables from "./schema" + +export const relations = defineRelations(tables, (r) => ({ + users: { + posts: r.many.posts({ from: r.users.id, to: r.posts.authorId }), + }, + posts: { + author: r.one.users({ from: r.posts.authorId, to: r.users.id }), + }, })) ``` diff --git a/website/package.json b/website/package.json index 636734e5..e9257f2b 100644 --- a/website/package.json +++ b/website/package.json @@ -50,8 +50,8 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.1", - "drizzle-orm": "^0.39.3", + "drizzle-kit": "1.0.0-beta.1-7946562", + "drizzle-orm": "1.0.0-beta.1-7946562", "fastify": "^4.28.1", "graphql": "^16.8.1", "graphql-scalars": "^1.24.1",