0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-10 22:38:53 -05:00

Move field schema only a schema object

This commit is contained in:
Matthew Phillips 2024-02-09 14:44:36 -05:00
parent 2a2539fbd2
commit 1d42585f24
6 changed files with 86 additions and 69 deletions

View file

@ -434,16 +434,16 @@ function isEmpty(obj: Record<string, unknown>) {
* @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column * @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column
*/ */
function canAlterTableAddColumn(field: DBField) { function canAlterTableAddColumn(field: DBField) {
if (field.unique) return false; if (field.schema.unique) return false;
if (hasRuntimeDefault(field)) return false; if (hasRuntimeDefault(field)) return false;
if (!field.optional && !hasDefault(field)) return false; if (!field.schema.optional && !hasDefault(field)) return false;
if (hasPrimaryKey(field)) return false; if (hasPrimaryKey(field)) return false;
if (getReferencesConfig(field)) return false; if (getReferencesConfig(field)) return false;
return true; return true;
} }
function canAlterTableDropColumn(field: DBField) { function canAlterTableDropColumn(field: DBField) {
if (field.unique) return false; if (field.schema.unique) return false;
if (hasPrimaryKey(field)) return false; if (hasPrimaryKey(field)) return false;
return true; return true;
} }
@ -461,10 +461,10 @@ function canRecreateTableWithoutDataLoss(
if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) { if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) {
return { dataLoss: true, fieldName, reason: 'added-required' }; return { dataLoss: true, fieldName, reason: 'added-required' };
} }
if (!a.optional && !hasDefault(a)) { if (!a.schema.optional && !hasDefault(a)) {
return { dataLoss: true, fieldName, reason: 'added-required' }; return { dataLoss: true, fieldName, reason: 'added-required' };
} }
if (!a.optional && a.unique) { if (!a.schema.optional && a.schema.unique) {
return { dataLoss: true, fieldName, reason: 'added-unique' }; return { dataLoss: true, fieldName, reason: 'added-unique' };
} }
} }
@ -546,7 +546,7 @@ function canChangeTypeWithoutQuery(oldField: DBField, newField: DBField) {
// Using `DBField` will not narrow `default` based on the column `type` // Using `DBField` will not narrow `default` based on the column `type`
// Handle each field separately // Handle each field separately
type WithDefaultDefined<T extends DBField> = T & Required<Pick<T, 'default'>>; type WithDefaultDefined<T extends DBField> = T & Required<Pick<T['schema'], 'default'>>;
type DBFieldWithDefault = type DBFieldWithDefault =
| WithDefaultDefined<TextField> | WithDefaultDefined<TextField>
| WithDefaultDefined<DateField> | WithDefaultDefined<DateField>
@ -555,5 +555,5 @@ type DBFieldWithDefault =
| WithDefaultDefined<JsonField>; | WithDefaultDefined<JsonField>;
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault { function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
return !!(field.default && isSerializedSQL(field.default)); return !!(field.schema.default && isSerializedSQL(field.schema.default));
} }

View file

@ -35,8 +35,8 @@ function generateTableType(name: string, collection: DBCollection): string {
{ {
// Only select fields Drizzle needs for inference // Only select fields Drizzle needs for inference
type: field.type, type: field.type,
optional: field.optional, optional: field.schema.optional,
default: field.default, default: field.schema.default,
}, },
]) ])
) )

View file

@ -123,7 +123,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D
`Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.` `Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.`
); );
} }
const referencedCollection = references[0]?.collection; const referencedCollection = references[0]?.schema.collection;
if (!referencedCollection) { if (!referencedCollection) {
throw new Error( throw new Error(
`Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.`
@ -132,7 +132,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D
const query = `FOREIGN KEY (${fields const query = `FOREIGN KEY (${fields
.map((f) => sqlite.escapeName(f)) .map((f) => sqlite.escapeName(f))
.join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references
.map((r) => sqlite.escapeName(r.name!)) .map((r) => sqlite.escapeName(r.schema.name!))
.join(', ')})`; .join(', ')})`;
queries.push(query); queries.push(query);
} }
@ -160,10 +160,10 @@ export function getModifiers(fieldName: string, field: DBField) {
if (hasPrimaryKey(field)) { if (hasPrimaryKey(field)) {
return ' PRIMARY KEY'; return ' PRIMARY KEY';
} }
if (!field.optional) { if (!field.schema.optional) {
modifiers += ' NOT NULL'; modifiers += ' NOT NULL';
} }
if (field.unique) { if (field.schema.unique) {
modifiers += ' UNIQUE'; modifiers += ' UNIQUE';
} }
if (hasDefault(field)) { if (hasDefault(field)) {
@ -171,7 +171,7 @@ export function getModifiers(fieldName: string, field: DBField) {
} }
const references = getReferencesConfig(field); const references = getReferencesConfig(field);
if (references) { if (references) {
const { collection, name } = references; const { collection, name } = references.schema;
if (!collection || !name) { if (!collection || !name) {
throw new Error( throw new Error(
`Invalid reference for field ${fieldName}. This is an unexpected error that should be reported to the Astro team.` `Invalid reference for field ${fieldName}. This is an unexpected error that should be reported to the Astro team.`
@ -186,12 +186,14 @@ export function getModifiers(fieldName: string, field: DBField) {
export function getReferencesConfig(field: DBField) { export function getReferencesConfig(field: DBField) {
const canHaveReferences = field.type === 'number' || field.type === 'text'; const canHaveReferences = field.type === 'number' || field.type === 'text';
if (!canHaveReferences) return undefined; if (!canHaveReferences) return undefined;
return field.references; return field.schema.references;
} }
// Using `DBField` will not narrow `default` based on the column `type` // Using `DBField` will not narrow `default` based on the column `type`
// Handle each field separately // Handle each field separately
type WithDefaultDefined<T extends DBField> = T & Required<Pick<T, 'default'>>; type WithDefaultDefined<T extends DBField> = T & {
schema: Required<Pick<T['schema'], 'default'>>
};
type DBFieldWithDefault = type DBFieldWithDefault =
| WithDefaultDefined<TextField> | WithDefaultDefined<TextField>
| WithDefaultDefined<DateField> | WithDefaultDefined<DateField>
@ -201,7 +203,7 @@ type DBFieldWithDefault =
// Type narrowing the default fails on union types, so use a type guard // Type narrowing the default fails on union types, so use a type guard
export function hasDefault(field: DBField): field is DBFieldWithDefault { export function hasDefault(field: DBField): field is DBFieldWithDefault {
if (field.default !== undefined) { if (field.schema.default !== undefined) {
return true; return true;
} }
if (hasPrimaryKey(field) && field.type === 'number') { if (hasPrimaryKey(field) && field.type === 'number') {
@ -222,8 +224,8 @@ function toDefault<T>(def: T | SQL<any>): string {
} }
function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string { function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string {
if (isSerializedSQL(column.default)) { if (isSerializedSQL(column.schema.default)) {
return column.default.sql; return column.schema.default.sql;
} }
switch (column.type) { switch (column.type) {
@ -231,11 +233,11 @@ function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): str
case 'number': case 'number':
case 'text': case 'text':
case 'date': case 'date':
return toDefault(column.default); return toDefault(column.schema.default);
case 'json': { case 'json': {
let stringified = ''; let stringified = '';
try { try {
stringified = JSON.stringify(column.default); stringified = JSON.stringify(column.schema.default);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( console.log(

View file

@ -29,9 +29,11 @@ const baseFieldSchema = z.object({
collection: z.string().optional(), collection: z.string().optional(),
}); });
const booleanFieldSchema = baseFieldSchema.extend({ const booleanFieldSchema = z.object({
type: z.literal('boolean'), type: z.literal('boolean'),
default: z.union([z.boolean(), sqlSchema]).optional(), schema: baseFieldSchema.extend({
default: z.union([z.boolean(), sqlSchema]).optional(),
}),
}); });
const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and(
@ -71,11 +73,10 @@ const numberFieldOptsSchema: z.ZodType<
}) })
); );
const numberFieldSchema = numberFieldOptsSchema.and( const numberFieldSchema = z.object({
z.object({ type: z.literal('number'),
type: z.literal('number'), schema: numberFieldOptsSchema
}) });
);
const textFieldBaseSchema = baseFieldSchema const textFieldBaseSchema = baseFieldSchema
.omit({ optional: true }) .omit({ optional: true })
@ -119,27 +120,30 @@ const textFieldOptsSchema: z.ZodType<
}) })
); );
const textFieldSchema = textFieldOptsSchema.and( const textFieldSchema = z.object({
z.object({ type: z.literal('text'),
type: z.literal('text'), schema: textFieldOptsSchema
})
);
const dateFieldSchema = baseFieldSchema.extend({
type: z.literal('date'),
default: z
.union([
sqlSchema,
// allow date-like defaults in user config,
// transform to ISO string for D1 storage
z.coerce.date().transform((d) => d.toISOString()),
])
.optional(),
}); });
const jsonFieldSchema = baseFieldSchema.extend({ const dateFieldSchema = z.object({
type: z.literal('date'),
schema: baseFieldSchema.extend({
default: z
.union([
sqlSchema,
// allow date-like defaults in user config,
// transform to ISO string for D1 storage
z.coerce.date().transform((d) => d.toISOString()),
])
.optional(),
})
});
const jsonFieldSchema = z.object({
type: z.literal('json'), type: z.literal('json'),
default: z.unknown().optional(), schema: baseFieldSchema.extend({
default: z.unknown().optional(),
})
}); });
const fieldSchema = z.union([ const fieldSchema = z.union([
@ -150,6 +154,7 @@ const fieldSchema = z.union([
jsonFieldSchema, jsonFieldSchema,
]); ]);
export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]); export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]);
const fieldsSchema = z.record(fieldSchema); const fieldsSchema = z.record(fieldSchema);
export const indexSchema = z.object({ export const indexSchema = z.object({
@ -350,20 +355,30 @@ type FieldOpts<T extends DBFieldInput> = Omit<T, 'type'>;
type NumberFieldOpts = z.input<typeof numberFieldOptsSchema>; type NumberFieldOpts = z.input<typeof numberFieldOptsSchema>;
type TextFieldOpts = z.input<typeof textFieldOptsSchema>; type TextFieldOpts = z.input<typeof textFieldOptsSchema>;
function createField<S extends string, T extends Record<string, unknown>>(type: S, schema: T) {
return {
type,
/**
* @internal
*/
schema
};
}
export const field = { export const field = {
number: <T extends NumberFieldOpts>(opts: T = {} as T) => { number: <T extends NumberFieldOpts>(opts: T = {} as T) => {
return { type: 'number', ...opts } satisfies T & { type: 'number' }; return createField('number', opts) satisfies { type: 'number' };
}, },
boolean: <T extends FieldOpts<BooleanFieldInput>>(opts: T = {} as T) => { boolean: <T extends FieldOpts<BooleanFieldInput>>(opts: T = {} as T) => {
return { type: 'boolean', ...opts } satisfies T & { type: 'boolean' }; return createField('boolean', opts) satisfies { type: 'boolean' }
}, },
text: <T extends TextFieldOpts>(opts: T = {} as T) => { text: <T extends TextFieldOpts>(opts: T = {} as T) => {
return { type: 'text', ...opts } satisfies T & { type: 'text' }; return createField('text', opts) satisfies { type: 'text' };
}, },
date<T extends FieldOpts<DateFieldInput>>(opts: T) { date<T extends FieldOpts<DateFieldInput>>(opts: T = {} as T) {
return { type: 'date', ...opts } satisfies T & { type: 'date' }; return createField('date', opts) satisfies { type: 'date' };
}, },
json<T extends FieldOpts<JsonFieldInput>>(opts: T) { json<T extends FieldOpts<JsonFieldInput>>(opts: T = {} as T) {
return { type: 'json', ...opts } satisfies T & { type: 'json' }; return createField('json', opts) satisfies { type: 'json' };
}, },
}; };

View file

@ -19,7 +19,7 @@ export type { Table } from './types.js';
export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js';
export function hasPrimaryKey(field: DBField) { export function hasPrimaryKey(field: DBField) {
return 'primaryKey' in field && !!field.primaryKey; return 'primaryKey' in field.schema && !!field.schema.primaryKey;
} }
// Exports a few common expressions // Exports a few common expressions
@ -99,42 +99,42 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo
c = text(fieldName); c = text(fieldName);
// Duplicate default logic across cases to preserve type inference. // Duplicate default logic across cases to preserve type inference.
// No clean generic for every column builder. // No clean generic for every column builder.
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default));
if (field.primaryKey === true) c = c.primaryKey(); if (field.schema.primaryKey === true) c = c.primaryKey();
break; break;
} }
case 'number': { case 'number': {
c = integer(fieldName); c = integer(fieldName);
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default));
if (field.primaryKey === true) c = c.primaryKey(); if (field.schema.primaryKey === true) c = c.primaryKey();
break; break;
} }
case 'boolean': { case 'boolean': {
c = integer(fieldName, { mode: 'boolean' }); c = integer(fieldName, { mode: 'boolean' });
if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default));
break; break;
} }
case 'json': case 'json':
c = jsonType(fieldName); c = jsonType(fieldName);
if (field.default !== undefined) c = c.default(field.default); if (field.schema.default !== undefined) c = c.default(field.schema.default);
break; break;
case 'date': { case 'date': {
// Parse dates as strings when in JSON serializable mode // Parse dates as strings when in JSON serializable mode
if (isJsonSerializable) { if (isJsonSerializable) {
c = text(fieldName); c = text(fieldName);
if (field.default !== undefined) { if (field.schema.default !== undefined) {
c = c.default(handleSerializedSQL(field.default)); c = c.default(handleSerializedSQL(field.schema.default));
} }
} else { } else {
c = dateType(fieldName); c = dateType(fieldName);
if (field.default !== undefined) { if (field.schema.default !== undefined) {
const def = handleSerializedSQL(field.default); const def = handleSerializedSQL(field.schema.default);
c = c.default( c = c.default(
def instanceof SQL def instanceof SQL
? def ? def
: // default comes pre-transformed to an ISO string for D1 storage. : // default comes pre-transformed to an ISO string for D1 storage.
// parse back to a Date for Drizzle. // parse back to a Date for Drizzle.
z.coerce.date().parse(field.default) z.coerce.date().parse(field.schema.default)
); );
} }
} }
@ -142,8 +142,8 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo
} }
} }
if (!field.optional) c = c.notNull(); if (!field.schema.optional) c = c.notNull();
if (field.unique) c = c.unique(); if (field.schema.unique) c = c.unique();
return c; return c;
} }

View file

@ -76,7 +76,7 @@ export type Column<T extends DBField['type'], S extends GeneratedConfig> = T ext
export type Table< export type Table<
TTableName extends string, TTableName extends string,
TFields extends Record<string, Pick<DBField, 'type' | 'default' | 'optional'>>, TFields extends Record<string, Pick<DBField, 'type' | 'schema'>>,
> = SQLiteTableWithColumns<{ > = SQLiteTableWithColumns<{
name: TTableName; name: TTableName;
schema: undefined; schema: undefined;
@ -92,7 +92,7 @@ export type Table<
: TFields[K] extends { primaryKey: true } : TFields[K] extends { primaryKey: true }
? true ? true
: false; : false;
notNull: TFields[K]['optional'] extends true ? false : true; notNull: TFields[K]['schema']['optional'] extends true ? false : true;
} }
>; >;
}; };