mirror of
https://github.com/withastro/astro.git
synced 2025-01-20 22:12:38 -05:00
feat: MVP to use libsql in dev
This commit is contained in:
parent
c6477107a2
commit
395a8a1400
11 changed files with 591 additions and 191 deletions
|
@ -7,7 +7,7 @@ import {
|
|||
type TextField,
|
||||
type collectionSchema,
|
||||
collectionsSchema,
|
||||
} from 'somewhere';
|
||||
} from './types.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const adjustedConfigSchema = z.object({
|
||||
|
@ -21,7 +21,7 @@ export const astroConfigWithDBValidator = z.object({
|
|||
});
|
||||
|
||||
export function defineCollection(
|
||||
userConfig: z.input<typeof collectionSchema>,
|
||||
userConfig: z.input<typeof collectionSchema>
|
||||
): z.input<typeof collectionSchema> {
|
||||
return userConfig;
|
||||
}
|
||||
|
|
14
packages/db/src/consts.ts
Normal file
14
packages/db/src/consts.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export const PACKAGE_NAME = JSON.parse(
|
||||
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
||||
).name;
|
||||
|
||||
export const INTERNAL_MOD_IMPORT = JSON.stringify(`${PACKAGE_NAME}/internal`);
|
||||
export const DRIZZLE_MOD_IMPORT = JSON.stringify(`${PACKAGE_NAME}/internal-drizzle`);
|
||||
|
||||
export const SUPPORTED_SEED_FILES = ['db.seed.js', 'db.seed.mjs', 'db.seed.mts', 'db.seed.ts'];
|
||||
|
||||
export const DB_TYPES_FILE = 'db-types.d.ts';
|
||||
|
||||
export const VIRTUAL_MODULE_ID = 'astro:db';
|
|
@ -1 +1,3 @@
|
|||
export { defineCollection, field } from './config.js';
|
||||
|
||||
export { integration as default } from './integration.js';
|
||||
|
|
33
packages/db/src/integration.ts
Normal file
33
packages/db/src/integration.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import type { AstroIntegration } from 'astro';
|
||||
import { vitePluginDb } from './vite-plugin-db.js';
|
||||
import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js';
|
||||
import { typegen } from './typegen.js';
|
||||
import { collectionsSchema } from './types.js';
|
||||
|
||||
export function integration(): AstroIntegration {
|
||||
return {
|
||||
name: 'astro:db',
|
||||
hooks: {
|
||||
async 'astro:config:setup'({ updateConfig, config }) {
|
||||
// TODO: refine where we load collections
|
||||
// @matthewp: may want to load collections by path at runtime
|
||||
const collections = collectionsSchema.parse(config.db?.collections ?? {});
|
||||
updateConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
// TODO: figure out when vite.Plugin doesn't line up with these types
|
||||
// @ts-ignore
|
||||
vitePluginDb({
|
||||
collections,
|
||||
root: config.root,
|
||||
}),
|
||||
// @ts-ignore
|
||||
vitePluginInjectEnvTs(config),
|
||||
],
|
||||
},
|
||||
});
|
||||
await typegen({ collections, root: config.root });
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
25
packages/db/src/internal-drizzle.ts
Normal file
25
packages/db/src/internal-drizzle.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Drizzle utilities we expose directly from `astro:db`
|
||||
export {
|
||||
sql,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
isNull,
|
||||
isNotNull,
|
||||
inArray,
|
||||
notInArray,
|
||||
exists,
|
||||
notExists,
|
||||
between,
|
||||
notBetween,
|
||||
like,
|
||||
notIlike,
|
||||
not,
|
||||
asc,
|
||||
desc,
|
||||
and,
|
||||
or,
|
||||
} from 'drizzle-orm';
|
|
@ -1,25 +0,0 @@
|
|||
import { createClient } from '@libsql/client';
|
||||
import type { DBCollections } from 'circle-rhyme-yes-measure';
|
||||
import { type SQL, sql } from 'drizzle-orm';
|
||||
import { LibSQLDatabase, drizzle } from 'drizzle-orm/libsql';
|
||||
import { getCreateTableQuery } from './cli/sync/queries.js';
|
||||
|
||||
export async function createLocalDb(collections: DBCollections) {
|
||||
const client = createClient({ url: ':memory:' });
|
||||
const db = drizzle(client);
|
||||
|
||||
await createDbTables(db, collections);
|
||||
return db;
|
||||
}
|
||||
|
||||
async function createDbTables(db: LibSQLDatabase, collections: DBCollections) {
|
||||
const setupQueries: SQL[] = [];
|
||||
for (const [name, collection] of Object.entries(collections)) {
|
||||
const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${name}`);
|
||||
const createQuery = sql.raw(getCreateTableQuery(name, collection));
|
||||
setupQueries.push(dropQuery, createQuery);
|
||||
}
|
||||
for (const q of setupQueries) {
|
||||
await db.run(q);
|
||||
}
|
||||
}
|
|
@ -1,112 +1,262 @@
|
|||
import type { ColumnBaseConfig, ColumnDataType } from 'drizzle-orm';
|
||||
import type { SQLiteColumn, SQLiteTableWithColumns, TableConfig } from 'drizzle-orm/sqlite-core';
|
||||
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
|
||||
import { createClient } from '@libsql/client';
|
||||
import type {
|
||||
BooleanField,
|
||||
DBCollection,
|
||||
DBCollections,
|
||||
DBField,
|
||||
DateField,
|
||||
FieldType,
|
||||
JsonField,
|
||||
NumberField,
|
||||
TextField,
|
||||
} from './types.js';
|
||||
import { type LibSQLDatabase, drizzle } from 'drizzle-orm/libsql';
|
||||
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
||||
import { bold } from 'kleur/colors';
|
||||
import { type SQL, type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm';
|
||||
import {
|
||||
customType,
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
type SQLiteColumnBuilderBase,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
import { z } from 'zod';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export { collectionToTable, createDb } from 'circle-rhyme-yes-measure';
|
||||
|
||||
export {
|
||||
sql,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
isNull,
|
||||
isNotNull,
|
||||
inArray,
|
||||
notInArray,
|
||||
exists,
|
||||
notExists,
|
||||
between,
|
||||
notBetween,
|
||||
like,
|
||||
notIlike,
|
||||
not,
|
||||
asc,
|
||||
desc,
|
||||
and,
|
||||
or,
|
||||
} from 'drizzle-orm';
|
||||
export type SqliteDB = SqliteRemoteDatabase;
|
||||
export type {
|
||||
AstroTable,
|
||||
AstroText,
|
||||
AstroDate,
|
||||
AstroBoolean,
|
||||
AstroNumber,
|
||||
AstroJson,
|
||||
AstroId,
|
||||
} from './types.js';
|
||||
|
||||
export type AstroTable<T extends Pick<TableConfig, 'name' | 'columns'>> = SQLiteTableWithColumns<
|
||||
T & {
|
||||
schema: undefined;
|
||||
dialect: 'sqlite';
|
||||
const sqlite = new SQLiteAsyncDialect();
|
||||
|
||||
export async function createDb(collections: DBCollections) {
|
||||
const client = createClient({ url: ':memory:' });
|
||||
const db = drizzle(client);
|
||||
|
||||
await createDbTables(db, collections);
|
||||
return db;
|
||||
}
|
||||
|
||||
async function createDbTables(db: LibSQLDatabase, collections: DBCollections) {
|
||||
const setupQueries: SQL[] = [];
|
||||
for (const [name, collection] of Object.entries(collections)) {
|
||||
const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${name}`);
|
||||
const createQuery = sql.raw(getCreateTableQuery(name, collection));
|
||||
setupQueries.push(dropQuery, createQuery);
|
||||
}
|
||||
>;
|
||||
|
||||
type GeneratedConfig<T extends ColumnDataType> = Pick<
|
||||
ColumnBaseConfig<T, string>,
|
||||
'name' | 'tableName' | 'notNull' | 'hasDefault'
|
||||
>;
|
||||
|
||||
export type AstroText<T extends GeneratedConfig<'string'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: string;
|
||||
dataType: 'string';
|
||||
columnType: 'SQLiteText';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
for (const q of setupQueries) {
|
||||
await db.run(q);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCreateTableQuery(collectionName: string, collection: DBCollection) {
|
||||
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
|
||||
|
||||
const colQueries = ['"id" text PRIMARY KEY'];
|
||||
for (const [columnName, column] of Object.entries(collection.fields)) {
|
||||
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
|
||||
column.type
|
||||
)}${getModifiers(columnName, column)}`;
|
||||
colQueries.push(colQuery);
|
||||
}
|
||||
|
||||
query += colQueries.join(', ') + ')';
|
||||
return query;
|
||||
}
|
||||
|
||||
function schemaTypeToSqlType(type: FieldType): 'text' | 'integer' {
|
||||
switch (type) {
|
||||
case 'date':
|
||||
case 'text':
|
||||
case 'json':
|
||||
return 'text';
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return 'integer';
|
||||
}
|
||||
}
|
||||
|
||||
function getModifiers(columnName: string, column: DBField) {
|
||||
let modifiers = '';
|
||||
if (!column.optional) {
|
||||
modifiers += ' NOT NULL';
|
||||
}
|
||||
if (column.unique) {
|
||||
modifiers += ' UNIQUE';
|
||||
}
|
||||
if (hasDefault(column)) {
|
||||
modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
// Using `DBField` will not narrow `default` based on the column `type`
|
||||
// Handle each field separately
|
||||
type WithDefaultDefined<T extends DBField> = T & Required<Pick<T, 'default'>>;
|
||||
type DBFieldWithDefault =
|
||||
| WithDefaultDefined<TextField>
|
||||
| WithDefaultDefined<DateField>
|
||||
| WithDefaultDefined<NumberField>
|
||||
| WithDefaultDefined<BooleanField>
|
||||
| WithDefaultDefined<JsonField>;
|
||||
|
||||
// Type narrowing the default fails on union types, so use a type guard
|
||||
function hasDefault(field: DBField): field is DBFieldWithDefault {
|
||||
return field.default !== undefined;
|
||||
}
|
||||
|
||||
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
|
||||
return field.type === 'date' && field.default === 'now';
|
||||
}
|
||||
|
||||
function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string {
|
||||
switch (column.type) {
|
||||
case 'boolean':
|
||||
return column.default ? 'TRUE' : 'FALSE';
|
||||
case 'number':
|
||||
return `${column.default}`;
|
||||
case 'text':
|
||||
return sqlite.escapeString(column.default);
|
||||
case 'date':
|
||||
return column.default === 'now' ? 'CURRENT_TIMESTAMP' : sqlite.escapeString(column.default);
|
||||
case 'json': {
|
||||
let stringified = '';
|
||||
try {
|
||||
stringified = JSON.stringify(column.default);
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Invalid default value for column ${bold(
|
||||
columnName
|
||||
)}. Defaults must be valid JSON when using the \`json()\` type.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return sqlite.escapeString(stringified);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return nanoid(12);
|
||||
}
|
||||
|
||||
const dateType = customType<{ data: Date; driverData: string }>({
|
||||
dataType() {
|
||||
return 'text';
|
||||
},
|
||||
toDriver(value) {
|
||||
return value.toISOString();
|
||||
},
|
||||
fromDriver(value) {
|
||||
return new Date(value);
|
||||
},
|
||||
});
|
||||
|
||||
const jsonType = customType<{ data: unknown; driverData: string }>({
|
||||
dataType() {
|
||||
return 'text';
|
||||
},
|
||||
toDriver(value) {
|
||||
return JSON.stringify(value);
|
||||
},
|
||||
fromDriver(value) {
|
||||
return JSON.parse(value);
|
||||
},
|
||||
});
|
||||
|
||||
const initialColumns = {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$default(() => generateId()),
|
||||
};
|
||||
|
||||
type D1ColumnBuilder = SQLiteColumnBuilderBase<
|
||||
ColumnBuilderBaseConfig<ColumnDataType, string> & { data: unknown }
|
||||
>;
|
||||
|
||||
export type AstroDate<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: Date;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
export function collectionToTable(
|
||||
name: string,
|
||||
collection: DBCollection,
|
||||
isJsonSerializable = true
|
||||
) {
|
||||
const columns: Record<string, D1ColumnBuilder> & typeof initialColumns = {
|
||||
// Spread to avoid mutating `initialColumns`
|
||||
...initialColumns,
|
||||
};
|
||||
|
||||
export type AstroBoolean<T extends GeneratedConfig<'boolean'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: boolean;
|
||||
dataType: 'boolean';
|
||||
columnType: 'SQLiteBoolean';
|
||||
driverParam: number;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
for (const [fieldName, field] of Object.entries(collection.fields)) {
|
||||
columns[fieldName] = columnMapper(fieldName, field, isJsonSerializable);
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroNumber<T extends GeneratedConfig<'number'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: number;
|
||||
dataType: 'number';
|
||||
columnType: 'SQLiteInteger';
|
||||
driverParam: number;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
const table = sqliteTable(name, columns);
|
||||
return table;
|
||||
}
|
||||
|
||||
export type AstroJson<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: unknown;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boolean) {
|
||||
let c: ReturnType<
|
||||
| typeof text
|
||||
| typeof integer
|
||||
| typeof jsonType
|
||||
| typeof dateType
|
||||
| typeof integer<string, 'boolean'>
|
||||
>;
|
||||
|
||||
export type AstroId<T extends Pick<GeneratedConfig<'string'>, 'tableName'>> = SQLiteColumn<
|
||||
T & {
|
||||
name: 'id';
|
||||
hasDefault: true;
|
||||
notNull: true;
|
||||
data: string;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
switch (field.type) {
|
||||
case 'text': {
|
||||
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);
|
||||
break;
|
||||
}
|
||||
case 'number': {
|
||||
c = integer(fieldName);
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
c = integer(fieldName, { mode: 'boolean' });
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
c = jsonType(fieldName);
|
||||
if (field.default !== undefined) c = c.default(field.default);
|
||||
break;
|
||||
case 'date': {
|
||||
// Parse dates as strings when in JSON serializable mode
|
||||
if (isJsonSerializable) {
|
||||
c = text(fieldName);
|
||||
if (field.default !== undefined) {
|
||||
c = c.default(field.default === 'now' ? sql`CURRENT_TIMESTAMP` : field.default);
|
||||
}
|
||||
} else {
|
||||
c = dateType(fieldName);
|
||||
if (field.default !== undefined) {
|
||||
c = c.default(
|
||||
field.default === 'now'
|
||||
? sql`CURRENT_TIMESTAMP`
|
||||
: // default comes pre-transformed to an ISO string for D1 storage.
|
||||
// parse back to a Date for Drizzle.
|
||||
z.coerce.date().parse(field.default)
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
>;
|
||||
|
||||
if (!field.optional) c = c.notNull();
|
||||
if (field.unique) c = c.unique();
|
||||
return c;
|
||||
}
|
||||
|
|
64
packages/db/src/typegen.ts
Normal file
64
packages/db/src/typegen.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import type { DBCollection, DBCollections, FieldType } from './types.js';
|
||||
import { DB_TYPES_FILE, DRIZZLE_MOD_IMPORT, INTERNAL_MOD_IMPORT } from './consts.js';
|
||||
|
||||
export async function typegen({ collections, root }: { collections: DBCollections; root: URL }) {
|
||||
const content = `// This file is generated by \`studio sync\`
|
||||
declare module 'astro:db' {
|
||||
export const db: import(${INTERNAL_MOD_IMPORT}).SqliteDB;
|
||||
export * from ${DRIZZLE_MOD_IMPORT}
|
||||
|
||||
${Object.entries(collections)
|
||||
.map(([name, collection]) => generateTableType(name, collection))
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
const dotAstroDir = new URL('.astro/', root);
|
||||
|
||||
if (!existsSync(dotAstroDir)) {
|
||||
await mkdir(dotAstroDir);
|
||||
}
|
||||
|
||||
await writeFile(new URL(DB_TYPES_FILE, dotAstroDir), content);
|
||||
}
|
||||
|
||||
function generateTableType(name: string, collection: DBCollection): string {
|
||||
let tableType = ` export const ${name}: import(${INTERNAL_MOD_IMPORT}).AstroTable<{
|
||||
name: ${JSON.stringify(name)};
|
||||
columns: {
|
||||
id: import(${INTERNAL_MOD_IMPORT}).AstroId<{
|
||||
tableName: ${JSON.stringify(name)};
|
||||
}>;`;
|
||||
|
||||
for (const [fieldName, field] of Object.entries(collection.fields)) {
|
||||
const drizzleInterface = schemaTypeToDrizzleInterface(field.type);
|
||||
tableType += `
|
||||
${fieldName}: import(${INTERNAL_MOD_IMPORT}).${drizzleInterface}<{
|
||||
tableName: ${JSON.stringify(name)};
|
||||
name: ${JSON.stringify(fieldName)};
|
||||
notNull: ${field.optional ? 'false' : 'true'};
|
||||
hasDefault: ${typeof field.default !== 'undefined' ? 'true' : 'false'};
|
||||
}>;`;
|
||||
}
|
||||
tableType += `
|
||||
};
|
||||
}>;`;
|
||||
return tableType;
|
||||
}
|
||||
|
||||
function schemaTypeToDrizzleInterface(type: FieldType) {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return 'AstroText';
|
||||
case 'number':
|
||||
return 'AstroNumber';
|
||||
case 'boolean':
|
||||
return 'AstroBoolean';
|
||||
case 'date':
|
||||
return 'AstroDate';
|
||||
case 'json':
|
||||
return 'AstroJson';
|
||||
}
|
||||
}
|
159
packages/db/src/types.ts
Normal file
159
packages/db/src/types.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm';
|
||||
import type { SQLiteColumn, SQLiteTableWithColumns, TableConfig } from 'drizzle-orm/sqlite-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
const baseFieldSchema = z.object({
|
||||
label: z.string().optional(),
|
||||
optional: z.boolean().optional(),
|
||||
unique: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const booleanFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('boolean'),
|
||||
default: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const numberFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional(),
|
||||
});
|
||||
|
||||
const textFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('text'),
|
||||
multiline: z.boolean().optional(),
|
||||
default: z.string().optional(),
|
||||
});
|
||||
|
||||
const dateFieldSchema = baseFieldSchema.extend({
|
||||
type: z.literal('date'),
|
||||
default: z
|
||||
.union([
|
||||
z.literal('now'),
|
||||
// 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({
|
||||
type: z.literal('json'),
|
||||
default: z.unknown().optional(),
|
||||
});
|
||||
|
||||
const fieldSchema = z.union([
|
||||
booleanFieldSchema,
|
||||
numberFieldSchema,
|
||||
textFieldSchema,
|
||||
dateFieldSchema,
|
||||
jsonFieldSchema,
|
||||
]);
|
||||
const fieldsSchema = z.record(fieldSchema);
|
||||
|
||||
export const collectionSchema = z.object({
|
||||
fields: fieldsSchema,
|
||||
});
|
||||
|
||||
export const collectionsSchema = z.record(collectionSchema);
|
||||
|
||||
export type BooleanField = z.infer<typeof booleanFieldSchema>;
|
||||
export type NumberField = z.infer<typeof numberFieldSchema>;
|
||||
export type TextField = z.infer<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 FieldType =
|
||||
| BooleanField['type']
|
||||
| NumberField['type']
|
||||
| TextField['type']
|
||||
| DateField['type']
|
||||
| JsonField['type'];
|
||||
|
||||
export type DBField = z.infer<typeof fieldSchema>;
|
||||
export type DBFieldInput = DateFieldInput | BooleanField | NumberField | TextField | JsonField;
|
||||
export type DBFields = z.infer<typeof fieldsSchema>;
|
||||
export type DBCollection = z.infer<typeof collectionSchema>;
|
||||
export type DBCollections = z.infer<typeof collectionsSchema>;
|
||||
|
||||
export type AstroTable<T extends Pick<TableConfig, 'name' | 'columns'>> = SQLiteTableWithColumns<
|
||||
T & {
|
||||
schema: undefined;
|
||||
dialect: 'sqlite';
|
||||
}
|
||||
>;
|
||||
|
||||
type GeneratedConfig<T extends ColumnDataType> = Pick<
|
||||
ColumnBaseConfig<T, string>,
|
||||
'name' | 'tableName' | 'notNull' | 'hasDefault'
|
||||
>;
|
||||
|
||||
export type AstroText<T extends GeneratedConfig<'string'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: string;
|
||||
dataType: 'string';
|
||||
columnType: 'SQLiteText';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroDate<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: Date;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroBoolean<T extends GeneratedConfig<'boolean'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: boolean;
|
||||
dataType: 'boolean';
|
||||
columnType: 'SQLiteBoolean';
|
||||
driverParam: number;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroNumber<T extends GeneratedConfig<'number'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: number;
|
||||
dataType: 'number';
|
||||
columnType: 'SQLiteInteger';
|
||||
driverParam: number;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroJson<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
|
||||
T & {
|
||||
data: unknown;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
||||
|
||||
export type AstroId<T extends Pick<GeneratedConfig<'string'>, 'tableName'>> = SQLiteColumn<
|
||||
T & {
|
||||
name: 'id';
|
||||
hasDefault: true;
|
||||
notNull: true;
|
||||
data: string;
|
||||
dataType: 'custom';
|
||||
columnType: 'SQLiteCustomColumn';
|
||||
driverParam: string;
|
||||
enumValues: never;
|
||||
baseColumn: never;
|
||||
}
|
||||
>;
|
|
@ -1,22 +1,24 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { DBCollections } from 'circle-rhyme-yes-measure';
|
||||
import { red } from 'kleur/colors';
|
||||
import {
|
||||
INTERNAL_LOCAL_PKG_IMP,
|
||||
INTERNAL_PKG_IMP,
|
||||
ROOT,
|
||||
DRIZZLE_MOD_IMPORT,
|
||||
INTERNAL_MOD_IMPORT,
|
||||
SUPPORTED_SEED_FILES,
|
||||
VIRTUAL_MODULE_ID,
|
||||
drizzleFilterExps,
|
||||
} from './consts.js';
|
||||
import type { VitePlugin } from './utils.js';
|
||||
import type { DBCollections } from './types.js';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
||||
type Opts = { mode: 'dev' } | { mode: 'prod'; projectId: string; token: string };
|
||||
|
||||
export function vitePluginDb(collections: DBCollections, opts: Opts): VitePlugin {
|
||||
export function vitePluginDb({
|
||||
collections,
|
||||
root,
|
||||
}: {
|
||||
collections: DBCollections;
|
||||
root: URL;
|
||||
}): VitePlugin {
|
||||
return {
|
||||
name: 'astro:db',
|
||||
enforce: 'pre',
|
||||
|
@ -27,34 +29,30 @@ export function vitePluginDb(collections: DBCollections, opts: Opts): VitePlugin
|
|||
},
|
||||
load(id) {
|
||||
if (id !== resolvedVirtualModuleId) return;
|
||||
|
||||
if (opts.mode === 'dev') {
|
||||
return getLocalVirtualModuleContents({ collections });
|
||||
}
|
||||
|
||||
return getProdVirtualModuleContents({
|
||||
collections,
|
||||
projectId: opts.projectId,
|
||||
appToken: opts.token,
|
||||
});
|
||||
return getLocalVirtualModuleContents({ collections, root });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const seedErrorMessage = `${red(
|
||||
'⚠️ Failed to seed data.',
|
||||
'⚠️ Failed to seed data.'
|
||||
)} Is the seed file out-of-date with recent schema changes?`;
|
||||
|
||||
export function getLocalVirtualModuleContents({ collections }: { collections: DBCollections }) {
|
||||
const seedFile = SUPPORTED_SEED_FILES.map((f) => fileURLToPath(new URL(f, ROOT))).find((f) =>
|
||||
existsSync(f),
|
||||
export function getLocalVirtualModuleContents({
|
||||
collections,
|
||||
root,
|
||||
}: {
|
||||
collections: DBCollections;
|
||||
root: URL;
|
||||
}) {
|
||||
const seedFile = SUPPORTED_SEED_FILES.map((f) => fileURLToPath(new URL(f, root))).find((f) =>
|
||||
existsSync(f)
|
||||
);
|
||||
return `
|
||||
import { collectionToTable } from ${INTERNAL_PKG_IMP};
|
||||
import { createLocalDb } from ${INTERNAL_LOCAL_PKG_IMP};
|
||||
import { collectionToTable, createDb } from ${INTERNAL_MOD_IMPORT};
|
||||
|
||||
export const db = await createLocalDb(${JSON.stringify(collections)});
|
||||
${drizzleFilterExps}
|
||||
export const db = await createDb(${JSON.stringify(collections)});
|
||||
export * from ${DRIZZLE_MOD_IMPORT};
|
||||
|
||||
${getStringifiedCollectionExports(collections)}
|
||||
|
||||
|
@ -70,32 +68,13 @@ ${
|
|||
`;
|
||||
}
|
||||
|
||||
export function getProdVirtualModuleContents({
|
||||
collections,
|
||||
projectId,
|
||||
appToken,
|
||||
}: {
|
||||
collections: DBCollections;
|
||||
projectId: string;
|
||||
appToken: string;
|
||||
}) {
|
||||
return `
|
||||
import { collectionToTable, createDb } from ${INTERNAL_PKG_IMP};
|
||||
|
||||
export const db = createDb(${JSON.stringify(projectId)}, ${JSON.stringify(appToken)});
|
||||
${drizzleFilterExps}
|
||||
|
||||
${getStringifiedCollectionExports(collections)}
|
||||
`;
|
||||
}
|
||||
|
||||
function getStringifiedCollectionExports(collections: DBCollections) {
|
||||
return Object.entries(collections)
|
||||
.map(
|
||||
([name, collection]) =>
|
||||
`export const ${name} = collectionToTable(${JSON.stringify(name)}, ${JSON.stringify(
|
||||
collection,
|
||||
)}, false)`,
|
||||
collection
|
||||
)}, false)`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
|
|
@ -2,40 +2,34 @@ import { existsSync } from 'node:fs';
|
|||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { AstroConfig } from 'astro';
|
||||
import { bold, cyan } from 'kleur/colors';
|
||||
import { normalizePath } from 'vite';
|
||||
import { DOT_ASTRO_DIR, DB_TYPES_FILE } from './consts.js';
|
||||
import type { VitePlugin } from './utils.js';
|
||||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import { DB_TYPES_FILE } from './consts.js';
|
||||
|
||||
export function getEnvTsPath({ srcDir }: { srcDir: URL }) {
|
||||
return new URL('env.d.ts', srcDir);
|
||||
}
|
||||
|
||||
export function vitePluginInjectEnvTs({ config }: { config: AstroConfig }): VitePlugin {
|
||||
export function vitePluginInjectEnvTs({ srcDir, root }: { srcDir: URL; root: URL }): VitePlugin {
|
||||
return {
|
||||
name: 'db-inject-env-ts',
|
||||
// Use `post` to ensure project setup is complete
|
||||
// Ex. `.astro` types have been written
|
||||
enforce: 'post',
|
||||
async config() {
|
||||
await setUpEnvTs({ config });
|
||||
await setUpEnvTs({ srcDir, root });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function setUpEnvTs({ config }: { config: AstroConfig }) {
|
||||
const envTsPath = getEnvTsPath(config);
|
||||
export async function setUpEnvTs({ srcDir, root }: { srcDir: URL; root: URL }) {
|
||||
const envTsPath = getEnvTsPath({ srcDir });
|
||||
const envTsPathRelativetoRoot = normalizePath(
|
||||
path.relative(fileURLToPath(config.root), fileURLToPath(envTsPath)),
|
||||
path.relative(fileURLToPath(root), fileURLToPath(envTsPath))
|
||||
);
|
||||
|
||||
if (existsSync(envTsPath)) {
|
||||
let typesEnvContents = await readFile(envTsPath, 'utf-8');
|
||||
const dotAstroDir = new URL('.astro/', root);
|
||||
|
||||
if (!existsSync(DOT_ASTRO_DIR)) return;
|
||||
if (!existsSync(dotAstroDir)) return;
|
||||
|
||||
const dbTypeReference = getDBTypeReference(config);
|
||||
const dbTypeReference = getDBTypeReference({ srcDir, dotAstroDir });
|
||||
|
||||
if (!typesEnvContents.includes(dbTypeReference)) {
|
||||
typesEnvContents = `${dbTypeReference}\n${typesEnvContents}`;
|
||||
|
@ -45,10 +39,15 @@ export async function setUpEnvTs({ config }: { config: AstroConfig }) {
|
|||
}
|
||||
}
|
||||
|
||||
function getDBTypeReference({ srcDir }: { srcDir: URL }) {
|
||||
function getDBTypeReference({ srcDir, dotAstroDir }: { srcDir: URL; dotAstroDir: URL }) {
|
||||
const dbTypesFile = new URL(DB_TYPES_FILE, dotAstroDir);
|
||||
const contentTypesRelativeToSrcDir = normalizePath(
|
||||
path.relative(fileURLToPath(srcDir), fileURLToPath(DB_TYPES_FILE)),
|
||||
path.relative(fileURLToPath(srcDir), fileURLToPath(dbTypesFile))
|
||||
);
|
||||
|
||||
return `/// <reference path=${JSON.stringify(contentTypesRelativeToSrcDir)} />`;
|
||||
}
|
||||
|
||||
function getEnvTsPath({ srcDir }: { srcDir: URL }) {
|
||||
return new URL('env.d.ts', srcDir);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue