diff --git a/.gitignore b/.gitignore index 5b9f60a3..79a3b524 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ dev-dist # vscode .vscode/settings.json +# zed +.zed/settings.json + # mise mise.local.toml .mise.local.toml diff --git a/bun.lock b/bun.lock index a3b8f47f..ff29a3de 100644 --- a/bun.lock +++ b/bun.lock @@ -201,6 +201,23 @@ "typescript": "^5.0.0", }, }, + "packages/database": { + "name": "@roppoh/database", + "dependencies": { + "drizzle-orm": "^0.44.6", + "kysely": "0.28.11", + "kysely-d1": "0.4.0", + }, + "devDependencies": { + "@types/pg": "8.16.0", + "drizzle-kit": "0.31.8", + }, + "peerDependencies": { + "@types/better-sqlite3": "7.6.13", + "better-sqlite3": "12.6.2", + "typescript": "^5.0.0", + }, + }, "packages/domain": { "name": "@roppoh/domain", "dependencies": { @@ -923,6 +940,8 @@ "@roppoh/better-auth-database": ["@roppoh/better-auth-database@workspace:packages/better-auth-database"], + "@roppoh/database": ["@roppoh/database@workspace:packages/database"], + "@roppoh/domain": ["@roppoh/domain@workspace:packages/domain"], "@roppoh/fujimatu": ["@roppoh/fujimatu@workspace:apps/fujimatsu"], @@ -1669,7 +1688,9 @@ "knip": ["knip@5.82.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.1", "minimist": "^1.2.8", "oxc-resolver": "^11.15.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-1nQk+5AcnkqL40kGQXfouzAEXkTR+eSrgo/8m1d0BMei4eAzFwghoXC4gOKbACgBiCof7hE8wkBVDsEvznf85w=="], - "kysely": ["kysely@0.28.10", "", {}, "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA=="], + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + + "kysely-d1": ["kysely-d1@0.4.0", "", { "peerDependencies": { "kysely": "*" } }, "sha512-wUcVvQNtm30OTfuo7Ad5vYJ1qHqPXOCZc+zWchVKNyuvqY3u8OuGw4gmUx1Ypdx2wRVFLHVQC9I7v0pTmF7Nkw=="], "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], @@ -1851,11 +1872,11 @@ "periscopic": ["periscopic@4.0.2", "", { "dependencies": { "@types/estree": "*", "is-reference": "^3.0.2", "zimmerframe": "^1.0.0" } }, "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA=="], - "pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + "pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="], "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - "pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], + "pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], @@ -2421,6 +2442,8 @@ "@better-auth/cli/drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], + "@better-auth/cli/pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + "@better-auth/core/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "@chevrotain/cst-dts-gen/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -2589,6 +2612,8 @@ "better-auth/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "better-auth/kysely": ["kysely@0.28.10", "", {}, "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA=="], + "buffer/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -2689,6 +2714,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@better-auth/cli/pg/pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts new file mode 100644 index 00000000..9e448423 --- /dev/null +++ b/packages/database/drizzle.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + casing: "snake_case", + dbCredentials: { + url: "file:database.sqlite", + }, + dialect: "sqlite", + migrations: { + prefix: "timestamp", + }, + out: "./src/migrations", + schema: "./src/schemas", +}); diff --git a/packages/database/package.json b/packages/database/package.json new file mode 100644 index 00000000..29e28c36 --- /dev/null +++ b/packages/database/package.json @@ -0,0 +1,27 @@ +{ + "dependencies": { + "drizzle-orm": "^0.44.6", + "kysely": "0.28.11", + "kysely-d1": "0.4.0" + }, + "devDependencies": { + "@types/pg": "8.16.0", + "drizzle-kit": "0.31.8" + }, + "exports": { + ".": "./src/index.ts", + "./client": "./src/client.ts" + }, + "name": "@roppoh/database", + "peerDependencies": { + "@types/better-sqlite3": "7.6.13", + "better-sqlite3": "12.6.2", + "typescript": "^5.0.0" + }, + "private": true, + "scripts": { + "check:type": "tsc --noEmit", + "generate:migration": "drizzle-kit generate --config='./drizzle.config.ts'" + }, + "type": "module" +} diff --git a/packages/database/src/client.ts b/packages/database/src/client.ts new file mode 100644 index 00000000..9cdc845f --- /dev/null +++ b/packages/database/src/client.ts @@ -0,0 +1,34 @@ +import type { Database as SqliteDatabase } from "better-sqlite3"; +import type { Kyselify } from "drizzle-orm/kysely"; +import { Kysely, SqliteDialect } from "kysely"; +import { D1Dialect } from "kysely-d1"; +import type { member, organization } from "./schemas"; + +export interface Database { + organization: Kyselify; + member: Kyselify; +} + +export type DatabaseClient = Kysely; + +export const createDatabaseClient = ( + args: + | { + type: "d1"; + database: ConstructorParameters[0]["database"]; + } + | { type: "sqlite"; database: SqliteDatabase }, +): DatabaseClient => { + switch (args.type) { + case "d1": { + return new Kysely({ + dialect: new D1Dialect({ database: args.database }), + }); + } + case "sqlite": { + return new Kysely({ + dialect: new SqliteDialect({ database: args.database }), + }); + } + } +}; diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts new file mode 100644 index 00000000..292f6057 --- /dev/null +++ b/packages/database/src/index.ts @@ -0,0 +1 @@ +export * from "./schemas"; diff --git a/packages/database/src/schemas/index.ts b/packages/database/src/schemas/index.ts new file mode 100644 index 00000000..eeccb376 --- /dev/null +++ b/packages/database/src/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./member"; +export * from "./organization"; diff --git a/packages/database/src/schemas/member.ts b/packages/database/src/schemas/member.ts new file mode 100644 index 00000000..71025de7 --- /dev/null +++ b/packages/database/src/schemas/member.ts @@ -0,0 +1,28 @@ +/** biome-ignore-all assist/source/useSortedKeys: for reachability */ +import { relations } from "drizzle-orm"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { organization } from "./organization"; + +export const member = sqliteTable( + "member", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + role: text("role").default("member").notNull(), + userId: text("user_id").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + }, + (table) => [ + index("member_organizationId_idx").on(table.organizationId), + index("member_userId_idx").on(table.userId), + ], +); + +export const memberRelation = relations(member, ({ one }) => ({ + organization: one(organization, { + fields: [member.organizationId], + references: [organization.id], + }), +})); diff --git a/packages/database/src/schemas/organization.ts b/packages/database/src/schemas/organization.ts new file mode 100644 index 00000000..e28484e4 --- /dev/null +++ b/packages/database/src/schemas/organization.ts @@ -0,0 +1,26 @@ +/** biome-ignore-all assist/source/useSortedKeys: for reachability */ +import { relations, sql } from "drizzle-orm"; +import { + integer, + sqliteTable, + text, + uniqueIndex, +} from "drizzle-orm/sqlite-core"; +import { member } from "./member"; + +export const organization = sqliteTable( + "organization", + { + id: text("id").primaryKey(), + slug: text("slug").notNull().unique(), + name: text("name").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .default(sql`now()`) + .notNull(), + }, + (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], +); + +export const organizationRelation = relations(organization, ({ many }) => ({ + members: many(member), +})); diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json new file mode 100644 index 00000000..725cb32d --- /dev/null +++ b/packages/database/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "rootDir": "." + }, + "exclude": [], + "extends": "../../tsconfig.json", + "include": ["src", "./*.ts"] +} diff --git a/packages/database/turbo.json b/packages/database/turbo.json new file mode 100644 index 00000000..d18b7b9c --- /dev/null +++ b/packages/database/turbo.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "extends": ["//"], + "tasks": { + "check:type": { + "inputs": [ + "src", + "$TURBO_ROOT$/tsconfig.json", + "./*.ts", + "./tsconfig.json" + ] + } + } +}