feat: success using forked drizzle-orm for switching null to undefined

pull/715/head
Travis Fischer 2025-04-27 17:51:22 +07:00
rodzic bf6fbb0e65
commit 861265199f
15 zmienionych plików z 636 dodań i 1050 usunięć

Wyświetl plik

@ -37,7 +37,8 @@
},
"dependencies": {
"@agentic/validators": "workspace:*",
"@fisch0920/drizzle-zod": "^0.7.2",
"@fisch0920/drizzle-orm": "^0.43.5",
"@fisch0920/drizzle-zod": "^0.7.7",
"@google-cloud/logging": "^11.2.0",
"@hono/node-server": "^1.14.1",
"@hono/sentry": "^1.2.1",
@ -46,7 +47,6 @@
"@paralleldrive/cuid2": "^2.2.2",
"@sentry/node": "^9.14.0",
"@workos-inc/node": "^7.47.0",
"drizzle-orm": "^0.43.0",
"eventid": "^2.0.1",
"exit-hook": "catalog:",
"hono": "^4.7.7",
@ -60,7 +60,6 @@
"zod-validation-error": "^3.4.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.9",
"drizzle-kit": "^0.31.0"
"@types/jsonwebtoken": "^9.0.9"
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import { drizzle } from '@fisch0920/drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env } from '@/lib/env'
@ -39,4 +39,4 @@ export {
notInArray,
notLike,
or
} from 'drizzle-orm'
} from '@fisch0920/drizzle-orm'

Wyświetl plik

@ -1,7 +1,13 @@
import { validators } from '@agentic/validators'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
jsonb,
pgTable,
text
} from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import { relations } from 'drizzle-orm'
import { boolean, index, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
import { projects } from './project'
import { teams } from './team'
@ -18,8 +24,6 @@ import {
createUpdateSchema,
cuid,
deploymentId,
optionalCuid,
optionalText,
projectId,
timestamps
} from './utils'
@ -32,7 +36,7 @@ export const deployments = pgTable(
...timestamps,
hash: text().notNull(),
version: optionalText(),
version: text(),
enabled: boolean().default(true).notNull(),
published: boolean().default(false).notNull(),
@ -43,7 +47,7 @@ export const deployments = pgTable(
userId: cuid()
.notNull()
.references(() => users.id),
teamId: optionalCuid().references(() => teams.id),
teamId: cuid().references(() => teams.id),
projectId: projectId()
.notNull()
.references(() => projects.id, {
@ -122,9 +126,6 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
})
export const deploymentSelectSchema = createSelectSchema(deployments, {
version: z.string().nonempty().optional(),
teamId: z.string().cuid2().optional(),
// build: z.object({}),
// env: z.object({}),
pricingPlans: z.array(pricingPlanSchema),

Wyświetl plik

@ -1,6 +1,5 @@
import { validators } from '@agentic/validators'
import { z } from '@hono/zod-openapi'
import { relations } from 'drizzle-orm'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
@ -8,7 +7,8 @@ import {
jsonb,
pgTable,
text
} from 'drizzle-orm/pg-core'
} from '@fisch0920/drizzle-orm/pg-core'
import { z } from '@hono/zod-openapi'
import { getProviderToken } from '@/lib/auth/get-provider-token'
@ -21,10 +21,9 @@ import {
createSelectSchema,
createUpdateSchema,
cuid,
optionalDeploymentId,
optionalStripeId,
optionalText,
deploymentId,
projectId,
stripeId,
timestamps
} from './utils'
@ -36,7 +35,7 @@ export const projects = pgTable(
...timestamps,
name: text().notNull(),
alias: optionalText(),
alias: text(),
userId: cuid()
.notNull()
@ -44,10 +43,10 @@ export const projects = pgTable(
teamId: cuid().notNull(),
// Most recently published Deployment if one exists
lastPublishedDeploymentId: optionalDeploymentId(),
lastPublishedDeploymentId: deploymentId(),
// Most recent Deployment if one exists
lastDeploymentId: optionalDeploymentId(),
lastDeploymentId: deploymentId(),
applicationFeePercent: integer().default(20).notNull(),
@ -55,7 +54,7 @@ export const projects = pgTable(
isStripeConnectEnabled: boolean().default(false).notNull(),
// All deployments share the same underlying proxy secret
_secret: optionalText(),
_secret: text(),
// Auth token used to access the saasify API on behalf of this project
_providerToken: text().notNull(),
@ -66,8 +65,8 @@ export const projects = pgTable(
_webhooks: jsonb().$type<Webhook[]>().default([]).notNull(),
// Stripe products corresponding to the stripe plans across deployments
stripeBaseProductId: optionalStripeId(),
stripeRequestProductId: optionalStripeId(),
stripeBaseProductId: stripeId(),
stripeRequestProductId: stripeId(),
// [metricSlug: string]: string
stripeMetricProductIds: jsonb()
@ -99,7 +98,7 @@ export const projects = pgTable(
// the stripeID utility.
// TODO: is it wise to share this between dev and prod?
// TODO: is it okay for this to be public?
_stripeAccountId: optionalStripeId()
_stripeAccountId: stripeId()
},
(table) => [
index('project_userId_idx').on(table.userId),
@ -159,17 +158,7 @@ export const projectInsertSchema = createInsertSchema(projects, {
})
export const projectSelectSchema = createSelectSchema(projects, {
alias: z.string().nonempty().optional(),
lastPublishedDeploymentId: z.string().nonempty().optional(),
lastDeploymentId: z.string().nonempty().optional(),
_secret: z.string().nonempty().optional(),
stripeBaseProductId: z.string().nonempty().optional(),
stripeRequestProductId: z.string().nonempty().optional(),
stripeMetricProductIds: z.record(z.string(), z.string()).optional(),
stripeMetricProductIds: z.record(z.string(), z.string()).optional()
// _webhooks: z.array(webhookSchema),
// _stripeCouponIds: z.record(z.string(), z.string()).optional(),
// _stripePlanIds: z
@ -181,7 +170,6 @@ export const projectSelectSchema = createSelectSchema(projects, {
// })
// )
// .optional()
_stripeAccountId: z.string().nonempty().optional()
})
.omit({
_secret: true,

Wyświetl plik

@ -1,15 +1,18 @@
import { z } from '@hono/zod-openapi'
import { relations } from 'drizzle-orm'
import { index, pgTable, primaryKey } from 'drizzle-orm/pg-core'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
pgTable,
primaryKey
} from '@fisch0920/drizzle-orm/pg-core'
import { teams } from './team'
import { users } from './user'
import {
createSelectSchema,
cuid,
optionalBoolean,
optionalTimestamp,
teamMemberRoleEnum,
timestamp,
timestamps
} from './utils'
@ -26,8 +29,8 @@ export const teamMembers = pgTable(
.references(() => teams.id, { onDelete: 'cascade' }),
role: teamMemberRoleEnum().default('user').notNull(),
confirmed: optionalBoolean().default(false).notNull(),
confirmedAt: optionalTimestamp()
confirmed: boolean().default(false).notNull(),
confirmedAt: timestamp()
},
(table) => [
primaryKey({ columns: [table.userId, table.teamId] }),
@ -49,7 +52,5 @@ export const teamMembersRelations = relations(teamMembers, ({ one }) => ({
})
}))
export const teamMemberSelectSchema = createSelectSchema(teamMembers, {
confirmed: z.boolean(),
confirmedAt: z.string().datetime().optional()
}).openapi('TeamMember')
export const teamMemberSelectSchema =
createSelectSchema(teamMembers).openapi('TeamMember')

Wyświetl plik

@ -1,5 +1,10 @@
import { relations } from 'drizzle-orm'
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core'
import { relations } from '@fisch0920/drizzle-orm'
import {
index,
pgTable,
text,
uniqueIndex
} from '@fisch0920/drizzle-orm/pg-core'
import { teamMembers } from './team-member'
import { users } from './user'

Wyświetl plik

@ -1,6 +1,5 @@
import { validators } from '@agentic/validators'
import { z } from '@hono/zod-openapi'
import { relations } from 'drizzle-orm'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
@ -8,7 +7,7 @@ import {
pgTable,
text,
uniqueIndex
} from 'drizzle-orm/pg-core'
} from '@fisch0920/drizzle-orm/pg-core'
import { sha256 } from '@/lib/utils'
@ -19,9 +18,8 @@ import {
createSelectSchema,
createUpdateSchema,
id,
optionalStripeId,
optionalText,
optionalTimestamp,
stripeId,
timestamp,
timestamps,
userRoleEnum
} from './utils'
@ -35,25 +33,25 @@ export const users = pgTable(
username: text().notNull().unique(),
role: userRoleEnum().default('user').notNull(),
email: optionalText().unique(),
password: optionalText(),
email: text().unique(),
password: text(),
// metadata
firstName: optionalText(),
lastName: optionalText(),
image: optionalText(),
firstName: text(),
lastName: text(),
image: text(),
emailConfirmed: boolean().default(false).notNull(),
emailConfirmedAt: optionalTimestamp(),
emailConfirmedAt: timestamp(),
emailConfirmToken: text().unique().default(sha256()).notNull(),
passwordResetToken: optionalText().unique(),
passwordResetToken: text().unique(),
isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
// third-party auth providers
providers: jsonb().$type<AuthProviders>().default({}).notNull(),
stripeCustomerId: optionalStripeId().unique()
stripeCustomerId: stripeId().unique()
},
(table) => [
uniqueIndex('user_email_idx').on(table.email),
@ -75,19 +73,7 @@ export const userInsertSchema = createInsertSchema(users, {
message: 'Invalid username'
}),
email: (schema) =>
schema.refine(
(email) => {
if (email) {
return validators.email(email)
}
return true
},
{
message: 'Invalid email'
}
),
email: (schema) => schema.email().optional(),
providers: authProvidersSchema.optional()
}).pick({
@ -100,19 +86,7 @@ export const userInsertSchema = createInsertSchema(users, {
})
export const userSelectSchema = createSelectSchema(users, {
email: z.string().email().optional(),
password: z.string().nonempty().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
image: z.string().nonempty().optional(),
emailConfirmedAt: z.string().datetime().optional(),
passwordResetToken: z.string().nonempty().optional(),
providers: authProvidersSchema,
stripeCustomerId: z.string().nonempty().optional()
providers: authProvidersSchema
}).openapi('User')
export const userUpdateSchema = createUpdateSchema(users)

Wyświetl plik

@ -1,15 +1,17 @@
import { type Equal, sql, type Writable } from '@fisch0920/drizzle-orm'
import {
pgEnum,
type PgTimestampBuilderInitial,
type PgTimestampConfig,
type PgTimestampStringBuilderInitial,
type PgVarcharBuilderInitial,
type PgVarcharConfig,
timestamp as timestampImpl,
varchar
} from '@fisch0920/drizzle-orm/pg-core'
import { createSchemaFactory } from '@fisch0920/drizzle-zod'
import { z } from '@hono/zod-openapi'
import { createId } from '@paralleldrive/cuid2'
import { sql, type Writable } from 'drizzle-orm'
import {
customType,
pgEnum,
type PgVarcharBuilderInitial,
type PgVarcharConfig,
timestamp,
varchar
} from 'drizzle-orm/pg-core'
export function cuid<U extends string, T extends Readonly<[U, ...U[]]>>(
config?: PgVarcharConfig<T | Writable<T>, never>
@ -45,9 +47,25 @@ export const id = varchar('id', { length: 24 })
.primaryKey()
.$defaultFn(createId)
/**
* Timestamp with mode `string`
*/
export function timestamp<
TMode extends PgTimestampConfig['mode'] & {} = 'string'
>(
config?: PgTimestampConfig<TMode>
): Equal<TMode, 'string'> extends true
? PgTimestampStringBuilderInitial<''>
: PgTimestampBuilderInitial<''> {
return timestampImpl<TMode>({
mode: 'string' as unknown as TMode,
...config
})
}
export const timestamps = {
createdAt: timestamp('createdAt', { mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updatedAt', { mode: 'string' })
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp()
.notNull()
.default(sql`now()`)
}
@ -55,7 +73,16 @@ export const timestamps = {
export const userRoleEnum = pgEnum('UserRole', ['user', 'admin'])
export const teamMemberRoleEnum = pgEnum('TeamMemberRole', ['user', 'admin'])
// TODO: Currently unused after forking drizzle-zod.
export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
createSchemaFactory({
zodInstance: z,
coerce: {
// Coerce dates / strings to timetamps
date: true
}
})
// TODO: Currently unused after forking @fisch0920/drizzle-zod.
// export function makeNullablePropsOptional<Schema extends z.AnyZodObject>(
// schema: Schema
// ): z.ZodObject<{
@ -77,102 +104,93 @@ export const teamMemberRoleEnum = pgEnum('TeamMemberRole', ['user', 'admin'])
// return z.object(newProps) as any
// }
export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
createSchemaFactory({
zodInstance: z,
coerce: {
// Coerce dates / strings to timetamps
date: true
}
})
// export type ColumnType =
// // string
// | 'text'
// | 'varchar'
// | 'timestamp'
// | 'stripeId'
// | 'projectId'
// | 'deploymentId'
// | 'cuid'
// // boolean
// | 'boolean'
// // number
// | 'integer'
// | 'smallint'
// | 'bigint'
// // json
// | 'json'
// | 'jsonb'
export type ColumnType =
// string
| 'text'
| 'varchar'
| 'timestamp'
| 'stripeId'
| 'projectId'
| 'deploymentId'
| 'cuid'
// boolean
| 'boolean'
// number
| 'integer'
| 'smallint'
| 'bigint'
// json
| 'json'
| 'jsonb'
// export type ColumnTypeToTSType<T extends ColumnType> = T extends
// | 'text'
// | 'varchar'
// | 'timestamp'
// | 'cuid'
// | 'stripeId'
// | 'projectId'
// | 'deploymentId'
// ? string
// : T extends 'boolean'
// ? boolean
// : T extends 'integer' | 'smallint' | 'bigint'
// ? number
// : never
export type ColumnTypeToTSType<T extends ColumnType> = T extends
| 'text'
| 'varchar'
| 'timestamp'
| 'cuid'
| 'stripeId'
| 'projectId'
| 'deploymentId'
? string
: T extends 'boolean'
? boolean
: T extends 'integer' | 'smallint' | 'bigint'
? number
: never
// /**
// * @see https://github.com/drizzle-team/@fisch0920/drizzle-orm/issues/2745
// */
// function optional<
// T extends ColumnType,
// InferredType extends
// | string
// | boolean
// | number
// | object = ColumnTypeToTSType<T>
// >(dataType: T) {
// return customType<{
// data: InferredType | undefined
// driverData: InferredType | null
// config: T extends 'stripeId'
// ? {
// length: number
// }
// : never
// }>({
// dataType() {
// if (dataType === 'stripeId') {
// return 'varchar({ length: 255 })'
// }
/**
* @see https://github.com/drizzle-team/drizzle-orm/issues/2745
*/
function optional<
T extends ColumnType,
InferredType extends
| string
| boolean
| number
| object = ColumnTypeToTSType<T>
>(dataType: T) {
return customType<{
data: InferredType | undefined
driverData: InferredType | null
config: T extends 'stripeId'
? {
length: number
}
: never
}>({
dataType() {
if (dataType === 'stripeId') {
return 'varchar({ length: 255 })'
}
// if (dataType === 'cuid') {
// return 'varchar({ length: 24 })'
// }
if (dataType === 'cuid') {
return 'varchar({ length: 24 })'
}
// if (dataType === 'projectId') {
// return 'varchar({ length: 130 })'
// }
if (dataType === 'projectId') {
return 'varchar({ length: 130 })'
}
// if (dataType === 'deploymentId') {
// return 'varchar({ length: 160 })'
// }
if (dataType === 'deploymentId') {
return 'varchar({ length: 160 })'
}
// if (dataType === 'timestamp') {
// return 'timestamp({ mode: "string" })'
// }
if (dataType === 'timestamp') {
return 'timestamp({ mode: "string" })'
}
// return dataType
// },
// fromDriver: (v) => v ?? undefined,
// toDriver: (v) => v ?? null
// })
// }
return dataType
},
fromDriver: (v) => v ?? undefined,
toDriver: (v) => v ?? null
})
}
export const optionalText = optional('text')
export const optionalTimestamp = optional('timestamp')
export const optionalBoolean = optional('boolean')
export const optionalVarchar = optional('varchar')
export const optionalCuid = optional('cuid')
export const optionalStripeId = optional('stripeId')
export const optionalProjectId = optional('projectId')
export const optionalDeploymentId = optional('deploymentId')
// export const optionalText = optional('text')
// export const optionalTimestamp = optional('timestamp')
// export const optionalBoolean = optional('boolean')
// export const optionalVarchar = optional('varchar')
// export const optionalCuid = optional('cuid')
// export const optionalStripeId = optional('stripeId')
// export const optionalProjectId = optional('projectId')
// export const optionalDeploymentId = optional('deploymentId')

Wyświetl plik

@ -0,0 +1,13 @@
import { expectTypeOf, test } from 'vitest'
import type { schema } from '.'
import type { User } from './types'
type UserSelect = typeof schema.users.$inferSelect
test('User types are compatible', () => {
// TODO: { github?: AuthProvider | undefined } !== { github: AuthProvider | undefined }
expectTypeOf<UserSelect>().toExtend<User>()
expectTypeOf<User[keyof User]>().toEqualTypeOf<UserSelect[keyof UserSelect]>()
})

Wyświetl plik

@ -1,5 +1,8 @@
import type {
BuildQueryResult,
ExtractTablesWithRelations
} from '@fisch0920/drizzle-orm'
import type { z } from '@hono/zod-openapi'
import type { BuildQueryResult, ExtractTablesWithRelations } from 'drizzle-orm'
import type * as schema from './schema'
@ -7,39 +10,30 @@ export type Tables = ExtractTablesWithRelations<typeof schema>
export type User = z.infer<typeof schema.userSelectSchema>
// export type User2 = typeof schema.users.$inferSelect
// export type User3 = NullToUndefinedDeep<typeof schema.users.$inferSelect>
export type Team = z.infer<typeof schema.teamSelectSchema>
export type TeamWithMembers = NullToUndefinedDeep<
BuildQueryResult<Tables, Tables['teams'], { with: { members: true } }>
export type TeamWithMembers = BuildQueryResult<
Tables,
Tables['teams'],
{ with: { members: true } }
>
export type TeamMember = z.infer<typeof schema.teamMemberSelectSchema>
export type TeamMemberWithTeam = NullToUndefinedDeep<
BuildQueryResult<Tables, Tables['teamMembers'], { with: { team: true } }>
export type TeamMemberWithTeam = BuildQueryResult<
Tables,
Tables['teamMembers'],
{ with: { team: true } }
>
export type Project = z.infer<typeof schema.projectSelectSchema>
export type ProjectWithLastPublishedDeployment = NullToUndefinedDeep<
BuildQueryResult<
Tables,
Tables['projects'],
{ with: { lastPublishedDeployment: true } }
>
export type ProjectWithLastPublishedDeployment = BuildQueryResult<
Tables,
Tables['projects'],
{ with: { lastPublishedDeployment: true } }
>
export type Deployment = z.infer<typeof schema.deploymentSelectSchema>
export type DeploymentWithProject = NullToUndefinedDeep<
BuildQueryResult<Tables, Tables['deployments'], { with: { project: true } }>
export type DeploymentWithProject = BuildQueryResult<
Tables,
Tables['deployments'],
{ with: { project: true } }
>
export type NullToUndefinedDeep<T> = T extends null
? undefined
: T extends Date
? T
: T extends readonly (infer U)[]
? NullToUndefinedDeep<U>[]
: T extends object
? { [K in keyof T]: NullToUndefinedDeep<T[K]> }
: T

Wyświetl plik

@ -1,9 +1,8 @@
import { eq } from 'drizzle-orm'
import { createMiddleware } from 'hono/factory'
import { jwt } from 'hono/jwt'
import type { AuthenticatedEnv } from '@/lib/types'
import { db, schema } from '@/db'
import { db, eq, schema } from '@/db'
import { env } from '@/lib/env'
import { assert } from '../utils'

Wyświetl plik

@ -1,4 +1,4 @@
import { and, eq } from 'drizzle-orm'
import { and, eq } from '@fisch0920/drizzle-orm'
import { createMiddleware } from 'hono/factory'
import type { AuthenticatedEnv } from '@/lib/types'

Wyświetl plik

@ -22,3 +22,14 @@ export type AuthenticatedEnv = {
}
export type AuthenticatedContext = Context<AuthenticatedEnv>
// TODO: currently unused
// export type NullToUndefinedDeep<T> = T extends null
// ? undefined
// : T extends Date
// ? T
// : T extends readonly (infer U)[]
// ? NullToUndefinedDeep<U>[]
// : T extends object
// ? { [K in keyof T]: NullToUndefinedDeep<T[K]> }
// : T

Wyświetl plik

@ -1,6 +1,7 @@
{
"extends": "@fisch0920/config/tsconfig-node",
"compilerOptions": {
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]

Plik diff jest za duży Load Diff