Skip to content

Commit

Permalink
export DB types (#10374)
Browse files Browse the repository at this point in the history
* export DB types

* refactor: move schemas to separate file

* chore: changeset

* chore: add typesVersions

---------

Co-authored-by: bholmesdev <hey@bholmes.dev>
  • Loading branch information
itsMapleLeaf and bholmesdev authored Mar 11, 2024
1 parent 33bfe6e commit f76dcb7
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 206 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-foxes-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/db": patch
---

Expose DB utility types from @astrojs/db/types
7 changes: 7 additions & 0 deletions packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@
"./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": {
"*": {
".": [
"./index.d.ts"
],
"types": [
"./dist/types.d.ts"
],
"utils": [
"./dist/utils.d.ts"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/core/cli/migration-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion packages/db/src/core/load-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
206 changes: 206 additions & 0 deletions packages/db/src/core/schemas.ts
Original file line number Diff line number Diff line change
@@ -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 | T[];

// Transform to serializable object for migration files
const sqlite = new SQLiteAsyncDialect();

const sqlSchema = z.instanceof(SQL<any>).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<typeof numberColumnBaseSchema> & {
// ReferenceableColumn creates a circular type. Define ZodType to resolve.
references?: NumberColumn;
},
ZodTypeDef,
z.input<typeof numberColumnBaseSchema> & {
references?: () => z.input<typeof numberColumnSchema>;
}
> = 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<typeof textColumnBaseSchema> & {
// ReferenceableColumn creates a circular type. Define ZodType to resolve.
references?: TextColumn;
},
ZodTypeDef,
z.input<typeof textColumnBaseSchema> & {
references?: () => z.input<typeof textColumnSchema>;
}
> = 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<string>;
references: () => MaybeArray<Omit<z.input<typeof referenceableColumnSchema>, 'references'>>;
};

type ForeignKeysOutput = Omit<ForeignKeysInput, 'references'> & {
// reference fn called in `transform`. Ensures output is JSON serializable.
references: MaybeArray<Omit<z.output<typeof referenceableColumnSchema>, 'references'>>;
};

const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInput> = 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(),
});
Loading

0 comments on commit f76dcb7

Please sign in to comment.