mirror of
https://github.com/withastro/astro.git
synced 2025-02-10 22:38:53 -05:00
fix: handle serialized SQL object correctly
This commit is contained in:
parent
377e086a2c
commit
522f4f2c92
6 changed files with 60 additions and 42 deletions
|
@ -6,7 +6,7 @@ import prompts from 'prompts';
|
|||
import type { Arguments } from 'yargs-parser';
|
||||
import { setupDbTables } from '../../../queries.js';
|
||||
import { getManagedAppTokenOrExit } from '../../../tokens.js';
|
||||
import type { AstroConfigWithDB, DBSnapshot } from '../../../types.js';
|
||||
import { collectionsSchema, type AstroConfigWithDB, type DBSnapshot } from '../../../types.js';
|
||||
import { getRemoteDatabaseUrl } from '../../../utils.js';
|
||||
import { getMigrationQueries } from '../../migration-queries.js';
|
||||
import {
|
||||
|
@ -28,7 +28,7 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
|
|||
|
||||
const migration = await getMigrationStatus(config);
|
||||
if (migration.state === 'no-migrations-found') {
|
||||
console.log(MIGRATIONS_NOT_INITIALIZED)
|
||||
console.log(MIGRATIONS_NOT_INITIALIZED);
|
||||
process.exit(1);
|
||||
} else if (migration.state === 'ahead') {
|
||||
console.log(MIGRATION_NEEDED);
|
||||
|
@ -42,12 +42,12 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
|
|||
const { data } = await prepareMigrateQuery({
|
||||
migrations: allLocalMigrations,
|
||||
appToken: appToken.token,
|
||||
})
|
||||
});
|
||||
missingMigrations = data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message.startsWith('{')) {
|
||||
const { error: { code } = { code: "" } } = JSON.parse(e.message);
|
||||
const { error: { code } = { code: '' } } = JSON.parse(e.message);
|
||||
if (code === 'TOKEN_UNAUTHORIZED') {
|
||||
console.error(MISSING_SESSION_ID_ERROR);
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ async function pushData({
|
|||
await setupDbTables({
|
||||
db,
|
||||
mode: 'build',
|
||||
collections: config.db.collections ?? {},
|
||||
collections: collectionsSchema.parse(config.db.collections ?? {}),
|
||||
data: config.db.data,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
|||
NumberField,
|
||||
TextField,
|
||||
} from '../types.js';
|
||||
import { SQL } from 'drizzle-orm';
|
||||
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import prompts from 'prompts';
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
schemaTypeToSqlType,
|
||||
} from '../queries.js';
|
||||
import { hasPrimaryKey } from '../../runtime/index.js';
|
||||
import { isSerializedSQL } from '../../runtime/types.js';
|
||||
|
||||
const sqlite = new SQLiteAsyncDialect();
|
||||
const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
@ -555,5 +555,5 @@ type DBFieldWithDefault =
|
|||
| WithDefaultDefined<JsonField>;
|
||||
|
||||
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
|
||||
return !!(field.default && field.default instanceof SQL);
|
||||
return !!(field.default && isSerializedSQL(field.default));
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
|||
import type { AstroIntegrationLogger } from 'astro';
|
||||
import type { DBUserConfig } from '../core/types.js';
|
||||
import { hasPrimaryKey } from '../runtime/index.js';
|
||||
import { isSerializedSQL } from '../runtime/types.js';
|
||||
|
||||
const sqlite = new SQLiteAsyncDialect();
|
||||
|
||||
|
@ -209,11 +210,9 @@ export function hasDefault(field: DBField): field is DBFieldWithDefault {
|
|||
return false;
|
||||
}
|
||||
|
||||
function toStringDefault<T>(def: T | SQL<any>): string {
|
||||
function toDefault<T>(def: T | SQL<any>): string {
|
||||
const type = typeof def;
|
||||
if (def instanceof SQL) {
|
||||
return sqlite.sqlToQuery(def).sql;
|
||||
} else if (type === 'string') {
|
||||
if (type === 'string') {
|
||||
return sqlite.escapeString(def as string);
|
||||
} else if (type === 'boolean') {
|
||||
return def ? 'TRUE' : 'FALSE';
|
||||
|
@ -223,12 +222,16 @@ function toStringDefault<T>(def: T | SQL<any>): string {
|
|||
}
|
||||
|
||||
function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string {
|
||||
if (isSerializedSQL(column.default)) {
|
||||
return sqlite.sqlToQuery(new SQL(column.default.queryChunks)).sql;
|
||||
}
|
||||
|
||||
switch (column.type) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'text':
|
||||
case 'date':
|
||||
return toStringDefault(column.default);
|
||||
return toDefault(column.default);
|
||||
case 'json': {
|
||||
let stringified = '';
|
||||
try {
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import type { SQLiteInsertValue } from 'drizzle-orm/sqlite-core';
|
||||
import { type SQLiteInsertValue } from 'drizzle-orm/sqlite-core';
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import { collectionToTable, type SqliteDB, type Table } from '../runtime/index.js';
|
||||
import { z, type ZodTypeDef } from 'zod';
|
||||
import { SQL } from 'drizzle-orm';
|
||||
import { errorMap } from './integration/error-map.js';
|
||||
import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js';
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
export type MaybeArray<T> = T | T[];
|
||||
|
||||
// Transform to serializable object for migration files
|
||||
const sqlSchema = z.instanceof(SQL<any>).transform(
|
||||
(sqlObj): SerializedSQL => ({
|
||||
[SERIALIZED_SQL_KEY]: true,
|
||||
queryChunks: sqlObj.queryChunks,
|
||||
})
|
||||
);
|
||||
|
||||
const baseFieldSchema = z.object({
|
||||
label: z.string().optional(),
|
||||
optional: z.boolean().optional(),
|
||||
|
@ -20,7 +29,7 @@ const baseFieldSchema = z.object({
|
|||
|
||||
const booleanFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('boolean'),
|
||||
default: z.union([z.boolean(), z.instanceof(SQL<any>)]).optional(),
|
||||
default: z.union([z.boolean(), sqlSchema]).optional(),
|
||||
});
|
||||
|
||||
const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and(
|
||||
|
@ -28,7 +37,7 @@ const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and(
|
|||
z.object({
|
||||
primaryKey: z.literal(false).optional(),
|
||||
optional: z.boolean().optional(),
|
||||
default: z.union([z.number(), z.instanceof(SQL<any>)]).optional(),
|
||||
default: z.union([z.number(), sqlSchema]).optional(),
|
||||
}),
|
||||
z.object({
|
||||
// `integer primary key` uses ROWID as the default value.
|
||||
|
@ -69,7 +78,7 @@ const numberFieldSchema = numberFieldOptsSchema.and(
|
|||
const textFieldBaseSchema = baseFieldSchema
|
||||
.omit({ optional: true })
|
||||
.extend({
|
||||
default: z.union([z.string(), z.instanceof(SQL<any>)]).optional(),
|
||||
default: z.union([z.string(), sqlSchema]).optional(),
|
||||
multiline: z.boolean().optional(),
|
||||
})
|
||||
.and(
|
||||
|
@ -118,7 +127,7 @@ const dateFieldSchema = baseFieldSchema.extend({
|
|||
type: z.literal('date'),
|
||||
default: z
|
||||
.union([
|
||||
z.instanceof(SQL<any>),
|
||||
sqlSchema,
|
||||
// allow date-like defaults in user config,
|
||||
// transform to ISO string for D1 storage
|
||||
z.coerce.date().transform((d) => d.toISOString()),
|
||||
|
@ -138,8 +147,7 @@ const fieldSchema = z.union([
|
|||
dateFieldSchema,
|
||||
jsonFieldSchema,
|
||||
]);
|
||||
export const referenceableFieldSchema = z.union([textFieldBaseSchema, numberFieldSchema]);
|
||||
export type ReferenceableField = z.input<typeof referenceableFieldSchema>;
|
||||
export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]);
|
||||
const fieldsSchema = z.record(fieldSchema);
|
||||
|
||||
export const indexSchema = z.object({
|
||||
|
@ -149,12 +157,12 @@ export const indexSchema = z.object({
|
|||
|
||||
type ForeignKeysInput = {
|
||||
fields: MaybeArray<string>;
|
||||
references: () => MaybeArray<Omit<ReferenceableField, 'references'>>;
|
||||
references: () => MaybeArray<Omit<z.input<typeof referenceableFieldSchema>, 'references'>>;
|
||||
};
|
||||
|
||||
type ForeignKeysOutput = Omit<ForeignKeysInput, 'references'> & {
|
||||
// reference fn called in `transform`. Ensures output is JSON serializable.
|
||||
references: MaybeArray<Omit<ReferenceableField, 'references'>>;
|
||||
references: MaybeArray<Omit<z.output<typeof referenceableFieldSchema>, 'references'>>;
|
||||
};
|
||||
|
||||
const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInput> = z.object({
|
||||
|
@ -204,14 +212,15 @@ export const collectionsSchema = z.preprocess((rawCollections) => {
|
|||
}, z.record(collectionSchema));
|
||||
|
||||
export type BooleanField = z.infer<typeof booleanFieldSchema>;
|
||||
export type BooleanFieldInput = z.input<typeof booleanFieldSchema>;
|
||||
export type NumberField = z.infer<typeof numberFieldSchema>;
|
||||
export type NumberFieldInput = z.input<typeof numberFieldSchema>;
|
||||
export type TextField = z.infer<typeof textFieldSchema>;
|
||||
export type TextFieldInput = z.input<typeof textFieldSchema>;
|
||||
export type DateField = z.infer<typeof dateFieldSchema>;
|
||||
// Type `Date` is the config input, `string` is the output for D1 storage
|
||||
export type DateFieldInput = z.input<typeof dateFieldSchema>;
|
||||
export type JsonField = z.infer<typeof jsonFieldSchema>;
|
||||
export type JsonFieldInput = z.input<typeof jsonFieldSchema>;
|
||||
|
||||
export type FieldType =
|
||||
| BooleanField['type']
|
||||
|
@ -223,10 +232,10 @@ export type FieldType =
|
|||
export type DBField = z.infer<typeof fieldSchema>;
|
||||
export type DBFieldInput =
|
||||
| DateFieldInput
|
||||
| BooleanField
|
||||
| BooleanFieldInput
|
||||
| NumberFieldInput
|
||||
| TextFieldInput
|
||||
| JsonField;
|
||||
| JsonFieldInput;
|
||||
export type DBFields = z.infer<typeof fieldsSchema>;
|
||||
export type DBCollection = z.infer<
|
||||
typeof readableCollectionSchema | typeof writableCollectionSchema
|
||||
|
@ -290,7 +299,7 @@ interface CollectionConfig<TFields extends FieldsConfig = FieldsConfig>
|
|||
foreignKeys?: Array<{
|
||||
fields: MaybeArray<Extract<keyof TFields, string>>;
|
||||
// TODO: runtime error if parent collection doesn't match for all fields. Can't put a generic here...
|
||||
references: () => MaybeArray<ReferenceableField>;
|
||||
references: () => MaybeArray<z.input<typeof referenceableFieldSchema>>;
|
||||
}>;
|
||||
indexes?: Record<string, IndexConfig<TFields>>;
|
||||
}
|
||||
|
@ -343,7 +352,7 @@ export const field = {
|
|||
number: <T extends NumberFieldOpts>(opts: T = {} as T) => {
|
||||
return { type: 'number', ...opts } satisfies T & { type: 'number' };
|
||||
},
|
||||
boolean: <T extends FieldOpts<BooleanField>>(opts: T = {} as T) => {
|
||||
boolean: <T extends FieldOpts<BooleanFieldInput>>(opts: T = {} as T) => {
|
||||
return { type: 'boolean', ...opts } satisfies T & { type: 'boolean' };
|
||||
},
|
||||
text: <T extends TextFieldOpts>(opts: T = {} as T) => {
|
||||
|
@ -352,7 +361,7 @@ export const field = {
|
|||
date<T extends FieldOpts<DateFieldInput>>(opts: T) {
|
||||
return { type: 'date', ...opts } satisfies T & { type: 'date' };
|
||||
},
|
||||
json<T extends FieldOpts<JsonField>>(opts: T) {
|
||||
json<T extends FieldOpts<JsonFieldInput>>(opts: T) {
|
||||
return { type: 'json', ...opts } satisfies T & { type: 'json' };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
type IndexBuilder,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
import { z } from 'zod';
|
||||
import { isSerializedSQL, type SerializedSQL } from './types.js';
|
||||
|
||||
export { sql };
|
||||
export type SqliteDB = SqliteRemoteDatabase;
|
||||
|
@ -98,19 +99,19 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo
|
|||
c = text(fieldName);
|
||||
// Duplicate default logic across cases to preserve type inference.
|
||||
// No clean generic for every column builder.
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default));
|
||||
if (field.primaryKey === true) c = c.primaryKey();
|
||||
break;
|
||||
}
|
||||
case 'number': {
|
||||
c = integer(fieldName);
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default));
|
||||
if (field.primaryKey === true) c = c.primaryKey();
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
c = integer(fieldName, { mode: 'boolean' });
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default));
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
|
@ -122,12 +123,12 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo
|
|||
if (isJsonSerializable) {
|
||||
c = text(fieldName);
|
||||
if (field.default !== undefined) {
|
||||
c = c.default(field.default);
|
||||
c = c.default(handleSerializedSQL(field.default));
|
||||
}
|
||||
} else {
|
||||
c = dateType(fieldName);
|
||||
if (field.default !== undefined) {
|
||||
const def = convertSerializedSQL(field.default);
|
||||
const def = handleSerializedSQL(field.default);
|
||||
c = c.default(
|
||||
def instanceof SQL
|
||||
? def
|
||||
|
@ -146,14 +147,9 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo
|
|||
return c;
|
||||
}
|
||||
|
||||
function isSerializedSQL(obj: unknown): boolean {
|
||||
return typeof obj === 'object' && !!(obj as any).queryChunks;
|
||||
}
|
||||
|
||||
function convertSerializedSQL<T = unknown>(obj: T): SQL<any> | T {
|
||||
if (isSerializedSQL(obj)) {
|
||||
return new SQL((obj as any).queryChunks);
|
||||
} else {
|
||||
return obj;
|
||||
function handleSerializedSQL<T>(def: T | SerializedSQL) {
|
||||
if (isSerializedSQL(def)) {
|
||||
return new SQL(def.queryChunks);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm';
|
||||
import type { ColumnDataType, ColumnBaseConfig, SQLChunk } from 'drizzle-orm';
|
||||
import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core';
|
||||
import type { DBField } from '../core/types.js';
|
||||
|
||||
|
@ -97,3 +97,13 @@ export type Table<
|
|||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
export const SERIALIZED_SQL_KEY = '__serializedSQL';
|
||||
export type SerializedSQL = {
|
||||
[SERIALIZED_SQL_KEY]: true;
|
||||
queryChunks: SQLChunk[];
|
||||
};
|
||||
|
||||
export function isSerializedSQL(value: any): value is SerializedSQL {
|
||||
return typeof value === 'object' && value !== null && SERIALIZED_SQL_KEY in value;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue