From 42bf8b41e4e19da7f71956b231d25d41b4a18bde Mon Sep 17 00:00:00 2001 From: itsMapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:24:03 -0600 Subject: [PATCH 1/4] export DB types --- packages/db/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/db/package.json b/packages/db/package.json index 071e51247a47..e05b832d6147 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,6 +23,10 @@ "./dist/runtime/config.js": { "import": "./dist/runtime/config.js" }, + "./types": { + "types": "./dist/core/types.d.ts", + "import": "./dist/core/types.js" + }, "./package.json": "./package.json" }, "typesVersions": { From 4f15e0e8ee35ce36e154c672df20d1d175501438 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 11 Mar 2024 17:53:57 -0400 Subject: [PATCH 2/4] refactor: move schemas to separate file --- packages/db/src/core/cli/migration-queries.ts | 2 +- packages/db/src/core/load-file.ts | 3 +- packages/db/src/core/schemas.ts | 206 +++++++++++++++++ packages/db/src/core/types.ts | 213 +----------------- packages/db/test/unit/column-queries.test.js | 2 +- packages/db/test/unit/index-queries.test.js | 2 +- .../db/test/unit/reference-queries.test.js | 2 +- 7 files changed, 224 insertions(+), 206 deletions(-) create mode 100644 packages/db/src/core/schemas.ts diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 3ff80d009e7b..3d77935977bc 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -30,9 +30,9 @@ import { type JsonColumn, type NumberColumn, type TextColumn, - columnSchema, } from '../types.js'; import { getRemoteDatabaseUrl } from '../utils.js'; +import { columnSchema } from '../schemas.js'; const sqlite = new SQLiteAsyncDialect(); const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts index 66a5e27c7527..dca49be337e4 100644 --- a/packages/db/src/core/load-file.ts +++ b/packages/db/src/core/load-file.ts @@ -8,7 +8,8 @@ import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js'; import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js'; import { errorMap } from './integration/error-map.js'; import { getConfigVirtualModContents } from './integration/vite-plugin-db.js'; -import { type AstroDbIntegration, dbConfigSchema } from './types.js'; +import { type AstroDbIntegration } from './types.js'; +import { dbConfigSchema } from './schemas.js'; import { getDbDirectoryUrl } from './utils.js'; const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration => diff --git a/packages/db/src/core/schemas.ts b/packages/db/src/core/schemas.ts new file mode 100644 index 000000000000..1305016cde21 --- /dev/null +++ b/packages/db/src/core/schemas.ts @@ -0,0 +1,206 @@ +import { SQL } from 'drizzle-orm'; +import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; +import { type ZodTypeDef, z } from 'zod'; +import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js'; +import { errorMap } from './integration/error-map.js'; +import type { NumberColumn, TextColumn } from './types.js'; + +export type MaybeArray = T | T[]; + +// Transform to serializable object for migration files +const sqlite = new SQLiteAsyncDialect(); + +const sqlSchema = z.instanceof(SQL).transform( + (sqlObj): SerializedSQL => ({ + [SERIALIZED_SQL_KEY]: true, + sql: sqlite.sqlToQuery(sqlObj).sql, + }) +); + +const baseColumnSchema = z.object({ + label: z.string().optional(), + optional: z.boolean().optional().default(false), + unique: z.boolean().optional().default(false), + deprecated: z.boolean().optional().default(false), + + // Defined when `defineReadableTable()` is called + name: z.string().optional(), + // TODO: rename to `tableName`. Breaking schema change + collection: z.string().optional(), +}); + +export const booleanColumnSchema = z.object({ + type: z.literal('boolean'), + schema: baseColumnSchema.extend({ + default: z.union([z.boolean(), sqlSchema]).optional(), + }), +}); + +const numberColumnBaseSchema = baseColumnSchema.omit({ optional: true }).and( + z.union([ + z.object({ + primaryKey: z.literal(false).optional().default(false), + optional: baseColumnSchema.shape.optional, + default: z.union([z.number(), sqlSchema]).optional(), + }), + z.object({ + // `integer primary key` uses ROWID as the default value. + // `optional` and `default` do not have an effect, + // so disable these config options for primary keys. + primaryKey: z.literal(true), + optional: z.literal(false).optional(), + default: z.literal(undefined).optional(), + }), + ]) +); + +export const numberColumnOptsSchema: z.ZodType< + z.infer & { + // ReferenceableColumn creates a circular type. Define ZodType to resolve. + references?: NumberColumn; + }, + ZodTypeDef, + z.input & { + references?: () => z.input; + } +> = numberColumnBaseSchema.and( + z.object({ + references: z + .function() + .returns(z.lazy(() => numberColumnSchema)) + .optional() + .transform((fn) => fn?.()), + }) +); + +export const numberColumnSchema = z.object({ + type: z.literal('number'), + schema: numberColumnOptsSchema, +}); + +const textColumnBaseSchema = baseColumnSchema + .omit({ optional: true }) + .extend({ + default: z.union([z.string(), sqlSchema]).optional(), + multiline: z.boolean().optional(), + }) + .and( + z.union([ + z.object({ + primaryKey: z.literal(false).optional().default(false), + optional: baseColumnSchema.shape.optional, + }), + z.object({ + // text primary key allows NULL values. + // NULL values bypass unique checks, which could + // lead to duplicate URLs per record in Astro Studio. + // disable `optional` for primary keys. + primaryKey: z.literal(true), + optional: z.literal(false).optional(), + }), + ]) + ); + +export const textColumnOptsSchema: z.ZodType< + z.infer & { + // ReferenceableColumn creates a circular type. Define ZodType to resolve. + references?: TextColumn; + }, + ZodTypeDef, + z.input & { + references?: () => z.input; + } +> = textColumnBaseSchema.and( + z.object({ + references: z + .function() + .returns(z.lazy(() => textColumnSchema)) + .optional() + .transform((fn) => fn?.()), + }) +); + +export const textColumnSchema = z.object({ + type: z.literal('text'), + schema: textColumnOptsSchema, +}); + +export const dateColumnSchema = z.object({ + type: z.literal('date'), + schema: baseColumnSchema.extend({ + default: z + .union([ + sqlSchema, + // transform to ISO string for serialization + z.date().transform((d) => d.toISOString()), + ]) + .optional(), + }), +}); + +export const jsonColumnSchema = z.object({ + type: z.literal('json'), + schema: baseColumnSchema.extend({ + default: z.unknown().optional(), + }), +}); + +export const columnSchema = z.union([ + booleanColumnSchema, + numberColumnSchema, + textColumnSchema, + dateColumnSchema, + jsonColumnSchema, +]); +export const referenceableColumnSchema = z.union([textColumnSchema, numberColumnSchema]); + +export const columnsSchema = z.record(columnSchema); + +export const indexSchema = z.object({ + on: z.string().or(z.array(z.string())), + unique: z.boolean().optional(), +}); + +type ForeignKeysInput = { + columns: MaybeArray; + references: () => MaybeArray, 'references'>>; +}; + +type ForeignKeysOutput = Omit & { + // reference fn called in `transform`. Ensures output is JSON serializable. + references: MaybeArray, 'references'>>; +}; + +const foreignKeysSchema: z.ZodType = z.object({ + columns: z.string().or(z.array(z.string())), + references: z + .function() + .returns(z.lazy(() => referenceableColumnSchema.or(z.array(referenceableColumnSchema)))) + .transform((fn) => fn()), +}); + +export const tableSchema = z.object({ + columns: columnsSchema, + indexes: z.record(indexSchema).optional(), + foreignKeys: z.array(foreignKeysSchema).optional(), + deprecated: z.boolean().optional().default(false), +}); + +export const tablesSchema = z.preprocess((rawTables) => { + // Use `z.any()` to avoid breaking object references + const tables = z.record(z.any()).parse(rawTables, { errorMap }); + for (const [tableName, table] of Object.entries(tables)) { + // Append table and column names to columns. + // Used to track table info for references. + const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap }); + for (const [columnName, column] of Object.entries(columns)) { + column.schema.name = columnName; + column.schema.collection = tableName; + } + } + return rawTables; +}, z.record(tableSchema)); + +export const dbConfigSchema = z.object({ + tables: tablesSchema.optional(), +}); diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index cff08c4a52a5..dc23ee509ed1 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -1,209 +1,24 @@ import type { AstroIntegration } from 'astro'; -import { SQL } from 'drizzle-orm'; -import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; -import { type ZodTypeDef, z } from 'zod'; -import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js'; -import { errorMap } from './integration/error-map.js'; - -export type MaybePromise = T | Promise; -export type MaybeArray = T | T[]; - -// Transform to serializable object for migration files -const sqlite = new SQLiteAsyncDialect(); - -const sqlSchema = z.instanceof(SQL).transform( - (sqlObj): SerializedSQL => ({ - [SERIALIZED_SQL_KEY]: true, - sql: sqlite.sqlToQuery(sqlObj).sql, - }) -); - -const baseColumnSchema = z.object({ - label: z.string().optional(), - optional: z.boolean().optional().default(false), - unique: z.boolean().optional().default(false), - deprecated: z.boolean().optional().default(false), - - // Defined when `defineReadableTable()` is called - name: z.string().optional(), - // TODO: rename to `tableName`. Breaking schema change - collection: z.string().optional(), -}); - -const booleanColumnSchema = z.object({ - type: z.literal('boolean'), - schema: baseColumnSchema.extend({ - default: z.union([z.boolean(), sqlSchema]).optional(), - }), -}); - -const numberColumnBaseSchema = baseColumnSchema.omit({ optional: true }).and( - z.union([ - z.object({ - primaryKey: z.literal(false).optional().default(false), - optional: baseColumnSchema.shape.optional, - default: z.union([z.number(), sqlSchema]).optional(), - }), - z.object({ - // `integer primary key` uses ROWID as the default value. - // `optional` and `default` do not have an effect, - // so disable these config options for primary keys. - primaryKey: z.literal(true), - optional: z.literal(false).optional(), - default: z.literal(undefined).optional(), - }), - ]) -); - -const numberColumnOptsSchema: z.ZodType< - z.infer & { - // ReferenceableColumn creates a circular type. Define ZodType to resolve. - references?: NumberColumn; - }, - ZodTypeDef, - z.input & { - references?: () => z.input; - } -> = numberColumnBaseSchema.and( - z.object({ - references: z - .function() - .returns(z.lazy(() => numberColumnSchema)) - .optional() - .transform((fn) => fn?.()), - }) -); - -const numberColumnSchema = z.object({ - type: z.literal('number'), - schema: numberColumnOptsSchema, -}); - -const textColumnBaseSchema = baseColumnSchema - .omit({ optional: true }) - .extend({ - default: z.union([z.string(), sqlSchema]).optional(), - multiline: z.boolean().optional(), - }) - .and( - z.union([ - z.object({ - primaryKey: z.literal(false).optional().default(false), - optional: baseColumnSchema.shape.optional, - }), - z.object({ - // text primary key allows NULL values. - // NULL values bypass unique checks, which could - // lead to duplicate URLs per record in Astro Studio. - // disable `optional` for primary keys. - primaryKey: z.literal(true), - optional: z.literal(false).optional(), - }), - ]) - ); - -const textColumnOptsSchema: z.ZodType< - z.infer & { - // ReferenceableColumn creates a circular type. Define ZodType to resolve. - references?: TextColumn; - }, - ZodTypeDef, - z.input & { - references?: () => z.input; - } -> = textColumnBaseSchema.and( - z.object({ - references: z - .function() - .returns(z.lazy(() => textColumnSchema)) - .optional() - .transform((fn) => fn?.()), - }) -); - -const textColumnSchema = z.object({ - type: z.literal('text'), - schema: textColumnOptsSchema, -}); - -const dateColumnSchema = z.object({ - type: z.literal('date'), - schema: baseColumnSchema.extend({ - default: z - .union([ - sqlSchema, - // transform to ISO string for serialization - z.date().transform((d) => d.toISOString()), - ]) - .optional(), - }), -}); - -const jsonColumnSchema = z.object({ - type: z.literal('json'), - schema: baseColumnSchema.extend({ - default: z.unknown().optional(), - }), -}); - -export const columnSchema = z.union([ +import type { z } from 'zod'; +import type { booleanColumnSchema, numberColumnSchema, textColumnSchema, dateColumnSchema, jsonColumnSchema, -]); -export const referenceableColumnSchema = z.union([textColumnSchema, numberColumnSchema]); - -const columnsSchema = z.record(columnSchema); - -export const indexSchema = z.object({ - on: z.string().or(z.array(z.string())), - unique: z.boolean().optional(), -}); - -type ForeignKeysInput = { - columns: MaybeArray; - references: () => MaybeArray, 'references'>>; -}; - -type ForeignKeysOutput = Omit & { - // reference fn called in `transform`. Ensures output is JSON serializable. - references: MaybeArray, 'references'>>; -}; - -const foreignKeysSchema: z.ZodType = z.object({ - columns: z.string().or(z.array(z.string())), - references: z - .function() - .returns(z.lazy(() => referenceableColumnSchema.or(z.array(referenceableColumnSchema)))) - .transform((fn) => fn()), -}); + columnSchema, + tableSchema, + referenceableColumnSchema, + indexSchema, + numberColumnOptsSchema, + textColumnOptsSchema, + columnsSchema, + MaybeArray, + dbConfigSchema, +} from './schemas.js'; export type Indexes = Record>; -export const tableSchema = z.object({ - columns: columnsSchema, - indexes: z.record(indexSchema).optional(), - foreignKeys: z.array(foreignKeysSchema).optional(), - deprecated: z.boolean().optional().default(false), -}); - -export const tablesSchema = z.preprocess((rawTables) => { - // Use `z.any()` to avoid breaking object references - const tables = z.record(z.any()).parse(rawTables, { errorMap }); - for (const [tableName, table] of Object.entries(tables)) { - // Append table and column names to columns. - // Used to track table info for references. - const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap }); - for (const [columnName, column] of Object.entries(columns)) { - column.schema.name = columnName; - column.schema.collection = tableName; - } - } - return rawTables; -}, z.record(tableSchema)); - export type BooleanColumn = z.infer; export type BooleanColumnInput = z.input; export type NumberColumn = z.infer; @@ -237,10 +52,6 @@ export type DBSnapshot = { version: string; }; -export const dbConfigSchema = z.object({ - tables: tablesSchema.optional(), -}); - export type DBConfigInput = z.input; export type DBConfig = z.infer; diff --git a/packages/db/test/unit/column-queries.test.js b/packages/db/test/unit/column-queries.test.js index 8ba6552f830f..c4b07a493786 100644 --- a/packages/db/test/unit/column-queries.test.js +++ b/packages/db/test/unit/column-queries.test.js @@ -5,7 +5,7 @@ import { getMigrationQueries, } from '../../dist/core/cli/migration-queries.js'; import { MIGRATION_VERSION } from '../../dist/core/consts.js'; -import { tableSchema } from '../../dist/core/types.js'; +import { tableSchema } from '../../dist/core/schemas.js'; import { column, defineTable } from '../../dist/runtime/config.js'; import { NOW } from '../../dist/runtime/index.js'; diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.js index ad588959d173..f5bde70e8e21 100644 --- a/packages/db/test/unit/index-queries.test.js +++ b/packages/db/test/unit/index-queries.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { tableSchema } from '../../dist/core/types.js'; +import { tableSchema } from '../../dist/core/schemas.js'; import { column } from '../../dist/runtime/config.js'; const userInitial = tableSchema.parse({ diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.js index a4b0bdd2d923..49d816f739f4 100644 --- a/packages/db/test/unit/reference-queries.test.js +++ b/packages/db/test/unit/reference-queries.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { tablesSchema } from '../../dist/core/types.js'; +import { tablesSchema } from '../../dist/core/schemas.js'; import { column, defineTable } from '../../dist/runtime/config.js'; const BaseUser = defineTable({ From 634975b05309f5fb7ba5c5a19df77ebad34c68a5 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 11 Mar 2024 17:54:57 -0400 Subject: [PATCH 3/4] chore: changeset --- .changeset/silver-foxes-protect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silver-foxes-protect.md diff --git a/.changeset/silver-foxes-protect.md b/.changeset/silver-foxes-protect.md new file mode 100644 index 000000000000..a7191eec2f1d --- /dev/null +++ b/.changeset/silver-foxes-protect.md @@ -0,0 +1,5 @@ +--- +"@astrojs/db": patch +--- + +Expose DB utility types from @astrojs/db/types From 0e8d8d5a71e1b9ad95b5231679dc82cb08b29446 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Mon, 11 Mar 2024 17:56:33 -0400 Subject: [PATCH 4/4] chore: add typesVersions --- packages/db/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/db/package.json b/packages/db/package.json index e05b832d6147..a99379d34a91 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -34,6 +34,9 @@ ".": [ "./index.d.ts" ], + "types": [ + "./dist/types.d.ts" + ], "utils": [ "./dist/utils.d.ts" ],