0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

export DB types (#10374)

* export DB types

* refactor: move schemas to separate file

* chore: changeset

* chore: add typesVersions

---------

Co-authored-by: bholmesdev <hey@bholmes.dev>
This commit is contained in:
Darius 2024-03-11 17:06:52 -05:00 committed by GitHub
parent 33bfe6ed05
commit f76dcb769f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 236 additions and 206 deletions

View file

@ -0,0 +1,5 @@
---
"@astrojs/db": patch
---
Expose DB utility types from @astrojs/db/types

View file

@ -23,6 +23,10 @@
"./dist/runtime/config.js": { "./dist/runtime/config.js": {
"import": "./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" "./package.json": "./package.json"
}, },
"typesVersions": { "typesVersions": {
@ -30,6 +34,9 @@
".": [ ".": [
"./index.d.ts" "./index.d.ts"
], ],
"types": [
"./dist/types.d.ts"
],
"utils": [ "utils": [
"./dist/utils.d.ts" "./dist/utils.d.ts"
], ],

View file

@ -30,9 +30,9 @@ import {
type JsonColumn, type JsonColumn,
type NumberColumn, type NumberColumn,
type TextColumn, type TextColumn,
columnSchema,
} from '../types.js'; } from '../types.js';
import { getRemoteDatabaseUrl } from '../utils.js'; import { getRemoteDatabaseUrl } from '../utils.js';
import { columnSchema } from '../schemas.js';
const sqlite = new SQLiteAsyncDialect(); const sqlite = new SQLiteAsyncDialect();
const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);

View file

@ -8,7 +8,8 @@ import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js';
import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js'; import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js';
import { errorMap } from './integration/error-map.js'; import { errorMap } from './integration/error-map.js';
import { getConfigVirtualModContents } from './integration/vite-plugin-db.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'; import { getDbDirectoryUrl } from './utils.js';
const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration => const isDbIntegration = (integration: AstroIntegration): integration is AstroDbIntegration =>

View file

@ -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(),
});

View file

@ -1,209 +1,24 @@
import type { AstroIntegration } from 'astro'; import type { AstroIntegration } from 'astro';
import { SQL } from 'drizzle-orm'; import type { z } from 'zod';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import type {
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> = T | Promise<T>;
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(),
});
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<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?.()),
})
);
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<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?.()),
})
);
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([
booleanColumnSchema, booleanColumnSchema,
numberColumnSchema, numberColumnSchema,
textColumnSchema, textColumnSchema,
dateColumnSchema, dateColumnSchema,
jsonColumnSchema, jsonColumnSchema,
]); columnSchema,
export const referenceableColumnSchema = z.union([textColumnSchema, numberColumnSchema]); tableSchema,
referenceableColumnSchema,
const columnsSchema = z.record(columnSchema); indexSchema,
numberColumnOptsSchema,
export const indexSchema = z.object({ textColumnOptsSchema,
on: z.string().or(z.array(z.string())), columnsSchema,
unique: z.boolean().optional(), MaybeArray,
}); dbConfigSchema,
} from './schemas.js';
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 type Indexes = Record<string, z.infer<typeof indexSchema>>; export type Indexes = Record<string, z.infer<typeof indexSchema>>;
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<typeof booleanColumnSchema>; export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
export type BooleanColumnInput = z.input<typeof booleanColumnSchema>; export type BooleanColumnInput = z.input<typeof booleanColumnSchema>;
export type NumberColumn = z.infer<typeof numberColumnSchema>; export type NumberColumn = z.infer<typeof numberColumnSchema>;
@ -237,10 +52,6 @@ export type DBSnapshot = {
version: string; version: string;
}; };
export const dbConfigSchema = z.object({
tables: tablesSchema.optional(),
});
export type DBConfigInput = z.input<typeof dbConfigSchema>; export type DBConfigInput = z.input<typeof dbConfigSchema>;
export type DBConfig = z.infer<typeof dbConfigSchema>; export type DBConfig = z.infer<typeof dbConfigSchema>;

View file

@ -5,7 +5,7 @@ import {
getMigrationQueries, getMigrationQueries,
} from '../../dist/core/cli/migration-queries.js'; } from '../../dist/core/cli/migration-queries.js';
import { MIGRATION_VERSION } from '../../dist/core/consts.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 { column, defineTable } from '../../dist/runtime/config.js';
import { NOW } from '../../dist/runtime/index.js'; import { NOW } from '../../dist/runtime/index.js';

View file

@ -1,7 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { describe, it } from 'mocha'; import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; 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'; import { column } from '../../dist/runtime/config.js';
const userInitial = tableSchema.parse({ const userInitial = tableSchema.parse({

View file

@ -1,7 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { describe, it } from 'mocha'; import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; 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'; import { column, defineTable } from '../../dist/runtime/config.js';
const BaseUser = defineTable({ const BaseUser = defineTable({