0
Fork 0
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:
bholmesdev 2024-02-09 13:30:58 -05:00
parent 377e086a2c
commit 522f4f2c92
6 changed files with 60 additions and 42 deletions

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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;
}

View file

@ -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;
}