@studiocms/migrator 0.0.0-beta.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/package.json +55 -7
  4. package/public/favicon.svg +5 -0
  5. package/src/components/ErrorCard.astro +20 -0
  6. package/src/components/MigrationMeta.astro +74 -0
  7. package/src/components/MigrationStatus.astro +71 -0
  8. package/src/components/PageHeader.astro +24 -0
  9. package/src/components/StatusTables.astro +45 -0
  10. package/src/db/astro-db-drizzle-client.ts +14 -0
  11. package/src/db/astro-db-schema.ts +193 -0
  12. package/src/db/astrodb.ts +88 -0
  13. package/src/db/client.ts +156 -0
  14. package/src/db/drizzle-schema.ts +54 -0
  15. package/src/env.d.ts +3 -0
  16. package/src/fonts/css/onest-variable.css +16 -0
  17. package/src/fonts/woff2/onest-variable.woff2 +0 -0
  18. package/src/layouts/Layout.astro +30 -0
  19. package/src/lib/astro-db-drizzle-compat/core-types.ts +88 -0
  20. package/src/lib/astro-db-drizzle-compat/error-map.ts +105 -0
  21. package/src/lib/astro-db-drizzle-compat/index.ts +149 -0
  22. package/src/lib/astro-db-drizzle-compat/schemas.ts +249 -0
  23. package/src/lib/astro-db-drizzle-compat/types.ts +141 -0
  24. package/src/lib/astro-db-drizzle-compat/utils.ts +55 -0
  25. package/src/lib/astro-db-drizzle-compat/virtual.ts +91 -0
  26. package/src/lib/errors.ts +57 -0
  27. package/src/lib/logger.ts +4 -0
  28. package/src/lib/remapUtils.ts +170 -0
  29. package/src/lib/response-utils.ts +12 -0
  30. package/src/lib/tableMap.ts +236 -0
  31. package/src/pages/data-migrations.ts +268 -0
  32. package/src/pages/index.astro +259 -0
  33. package/src/pages/schema-migrations.ts +31 -0
  34. package/src/styles/global.css +165 -0
  35. package/start.mjs +163 -0
  36. package/utils/logger.mjs +93 -0
  37. package/utils/resolver.mjs +60 -0
@@ -0,0 +1,88 @@
1
+ import { type AstroDBTableKeys, astroDbTableKeys } from '../lib/tableMap';
2
+ import { db, sql } from './astro-db-drizzle-client.js';
3
+
4
+ export const runConnectionTest = async () => {
5
+ try {
6
+ await db.$client.execute('SELECT CURRENT_TIME');
7
+ return true;
8
+ } catch (_error) {
9
+ return false;
10
+ }
11
+ };
12
+
13
+ /**
14
+ * Search for tables in a LibSQL database by name using Drizzle
15
+ * @param searchPattern - Search pattern (supports wildcards: %)
16
+ * @returns Array of matching table names
17
+ */
18
+ export async function searchTables(searchPattern: string): Promise<string[]> {
19
+ const result = await db.all<{ name: string }>(
20
+ sql`
21
+ SELECT name
22
+ FROM sqlite_master
23
+ WHERE type = 'table'
24
+ AND name LIKE ${searchPattern}
25
+ AND name NOT LIKE 'sqlite_%'
26
+ AND name != '_astro_db_snapshot'
27
+ ORDER BY name
28
+ `
29
+ );
30
+
31
+ return result.map((row) => row.name);
32
+ }
33
+
34
+ /**
35
+ * Get all tables in the database
36
+ * @returns Array of all table names
37
+ */
38
+ export async function getAllTables(): Promise<string[]> {
39
+ return searchTables('%');
40
+ }
41
+
42
+ /**
43
+ * Check if a specific table exists
44
+ * @param tableName - Exact table name to check
45
+ * @returns Boolean indicating if table exists
46
+ */
47
+ export async function tableExists(tableName: string): Promise<boolean> {
48
+ const tables = await searchTables(tableName);
49
+ return tables.length > 0;
50
+ }
51
+
52
+ export const getTableLength = async (table: AstroDBTableKeys) => {
53
+ const result = await db.get<{ count: number }>(
54
+ sql`SELECT COUNT(*) as count FROM ${sql.raw(table)}`
55
+ );
56
+
57
+ return result?.count ?? 0;
58
+ };
59
+
60
+ export const getDataMigrationMeta = async () => {
61
+ const tableLengths: Record<AstroDBTableKeys, number> = {} as Record<AstroDBTableKeys, number>;
62
+
63
+ for (const table of astroDbTableKeys) {
64
+ const result = await getTableLength(table).catch(() => 0);
65
+ tableLengths[table] = result;
66
+ }
67
+
68
+ return tableLengths;
69
+ };
70
+
71
+ // Usage examples:
72
+ // async function examples() {
73
+ // // Search for tables starting with "user"
74
+ // const userTables = await searchTables('user%');
75
+ // console.log('Tables starting with "user":', userTables);
76
+
77
+ // // Search for tables containing "post"
78
+ // const postTables = await searchTables('%post%');
79
+ // console.log('Tables containing "post":', postTables);
80
+
81
+ // // Get all tables
82
+ // const allTables = await getAllTables();
83
+ // console.log('All tables:', allTables);
84
+
85
+ // // Check if specific table exists
86
+ // const exists = await tableExists('users');
87
+ // console.log('Table "users" exists:', exists);
88
+ // }
@@ -0,0 +1,156 @@
1
+ import { runEffect } from '@withstudiocms/effect';
2
+ import { getDBClientLive } from '@withstudiocms/kysely/client';
3
+ import { sql } from '@withstudiocms/kysely/kysely';
4
+ import { getMigratorLive } from '@withstudiocms/sdk/migrator';
5
+ import type { StudioCMSDatabaseSchema } from '@withstudiocms/sdk/tables';
6
+ import { type KyselyTableKeys, kyselyTableKeys } from '../lib/tableMap';
7
+
8
+ /**
9
+ * Dynamically import and return the database driver based on the provided dialect.
10
+ *
11
+ * @param dialect - The database dialect as a string.
12
+ * @returns The database driver.
13
+ * @throws Error if the dialect is unsupported.
14
+ */
15
+ async function getDialectDriver(dialect: string | undefined) {
16
+ switch (dialect) {
17
+ case 'libsql': {
18
+ const mod = await import('@withstudiocms/kysely/drivers/libsql');
19
+ return await runEffect(mod.libsqlDriver);
20
+ }
21
+ case 'postgres': {
22
+ const mod = await import('@withstudiocms/kysely/drivers/postgres');
23
+ return await runEffect(mod.postgresDriver);
24
+ }
25
+ case 'mysql': {
26
+ const mod = await import('@withstudiocms/kysely/drivers/mysql');
27
+ return await runEffect(mod.mysqlDriver);
28
+ }
29
+ default:
30
+ throw new Error(`Unsupported STUDIOCMS_DIALECT: ${dialect}`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get a StudioCMS database client based on the STUDIOCMS_DIALECT environment variable.
36
+ *
37
+ * @returns The StudioCMS database client.
38
+ * @throws Error if the STUDIOCMS_DIALECT environment variable is not set or unsupported.
39
+ */
40
+ export const getStudioCMSDb = async () => {
41
+ const dialect = process.env.STUDIOCMS_DIALECT;
42
+ if (!dialect) {
43
+ throw new Error('STUDIOCMS_DIALECT environment variable is not set.');
44
+ }
45
+
46
+ const driver = await getDialectDriver(dialect);
47
+ return await runEffect(getDBClientLive<StudioCMSDatabaseSchema>(driver));
48
+ };
49
+
50
+ /**
51
+ * Get a StudioCMS database migrator based on the STUDIOCMS_DIALECT environment variable.
52
+ *
53
+ * @returns The StudioCMS database migrator.
54
+ * @throws Error if the STUDIOCMS_DIALECT environment variable is not set or unsupported.
55
+ */
56
+ export const studioCMSDbMigrator = async () => {
57
+ const dialect = process.env.STUDIOCMS_DIALECT;
58
+ if (!dialect) {
59
+ throw new Error('STUDIOCMS_DIALECT environment variable is not set.');
60
+ }
61
+
62
+ const driver = await getDialectDriver(dialect);
63
+ return await runEffect(getMigratorLive(driver));
64
+ };
65
+
66
+ /**
67
+ * Run a connection test to the StudioCMS database.
68
+ *
69
+ * @returns Boolean indicating if the connection test was successful.
70
+ */
71
+ export const runConnectionTest = async () => {
72
+ const { db } = await getStudioCMSDb();
73
+ try {
74
+ await db.executeQuery(sql`SELECT CURRENT_TIME;`.compile(db));
75
+ return true;
76
+ } catch (_e) {
77
+ return false;
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Map of supported database dialects to their display names.
83
+ */
84
+ const dialectMap = {
85
+ libsql: 'LibSQL',
86
+ mysql: 'MySQL',
87
+ postgres: 'PostgreSQL',
88
+ };
89
+
90
+ /**
91
+ * Get the database dialect from the environment variable.
92
+ *
93
+ * @returns The display name of the database dialect.
94
+ */
95
+ export const getDialectFromEnv = () => {
96
+ return dialectMap[process.env.STUDIOCMS_DIALECT as keyof typeof dialectMap] || 'Unknown';
97
+ };
98
+
99
+ /**
100
+ * Get the number of rows in a specified table.
101
+ *
102
+ * @param table - The name of the table.
103
+ * @returns The number of rows in the table.
104
+ */
105
+ export const getTableLength = async (table: KyselyTableKeys) => {
106
+ const { db } = await getStudioCMSDb();
107
+ const result = await db.selectFrom(table).select(sql`COUNT(*)`.as('count')).executeTakeFirst();
108
+
109
+ if (!result) {
110
+ return 0;
111
+ }
112
+
113
+ return Number(result.count);
114
+ };
115
+
116
+ /**
117
+ * Check the data migration status by verifying if any tables contain rows.
118
+ *
119
+ * @returns An object indicating whether migration can proceed.
120
+ */
121
+ export const getDataMigrationStatus = async () => {
122
+ const tablesWithRows: string[] = [];
123
+
124
+ for (const table of kyselyTableKeys) {
125
+ const result = await getTableLength(table).catch(() => 0);
126
+ if (result > 0) {
127
+ tablesWithRows.push(table);
128
+ }
129
+ }
130
+
131
+ if (tablesWithRows.length > 0) {
132
+ return {
133
+ canMigrate: false,
134
+ };
135
+ }
136
+
137
+ return {
138
+ canMigrate: true,
139
+ };
140
+ };
141
+
142
+ /**
143
+ * Get metadata about the data migration, including the number of rows in each table.
144
+ *
145
+ * @returns An object mapping table names to their respective row counts.
146
+ */
147
+ export const getDataMigrationMeta = async () => {
148
+ const tableLengths: Record<KyselyTableKeys, number> = {} as Record<KyselyTableKeys, number>;
149
+
150
+ for (const table of kyselyTableKeys) {
151
+ const result = await getTableLength(table).catch(() => 0);
152
+ tableLengths[table] = result;
153
+ }
154
+
155
+ return tableLengths;
156
+ };
@@ -0,0 +1,54 @@
1
+ import { asDrizzleTable } from '../lib/astro-db-drizzle-compat/index.js';
2
+ import * as Schema from './astro-db-schema.js';
3
+
4
+ export const StudioCMSUsers = asDrizzleTable('StudioCMSUsers', Schema.StudioCMSUsers);
5
+ export const StudioCMSPageData = asDrizzleTable('StudioCMSPageData', Schema.StudioCMSPageData);
6
+ export const StudioCMSPageFolderStructure = asDrizzleTable(
7
+ 'StudioCMSPageFolderStructure',
8
+ Schema.StudioCMSPageFolderStructure
9
+ );
10
+ export const StudioCMSPageDataTags = asDrizzleTable(
11
+ 'StudioCMSPageDataTags',
12
+ Schema.StudioCMSPageDataTags
13
+ );
14
+ export const StudioCMSPageDataCategories = asDrizzleTable(
15
+ 'StudioCMSPageDataCategories',
16
+ Schema.StudioCMSPageDataCategories
17
+ );
18
+ export const StudioCMSPluginData = asDrizzleTable(
19
+ 'StudioCMSPluginData',
20
+ Schema.StudioCMSPluginData
21
+ );
22
+ export const StudioCMSDynamicConfigSettings = asDrizzleTable(
23
+ 'StudioCMSDynamicConfigSettings',
24
+ Schema.StudioCMSDynamicConfigSettings
25
+ );
26
+ export const StudioCMSAPIKeys = asDrizzleTable('StudioCMSAPIKeys', Schema.StudioCMSAPIKeys);
27
+ export const StudioCMSUserResetTokens = asDrizzleTable(
28
+ 'StudioCMSUserResetTokens',
29
+ Schema.StudioCMSUserResetTokens
30
+ );
31
+ export const StudioCMSOAuthAccounts = asDrizzleTable(
32
+ 'StudioCMSOAuthAccounts',
33
+ Schema.StudioCMSOAuthAccounts
34
+ );
35
+ export const StudioCMSSessionTable = asDrizzleTable(
36
+ 'StudioCMSSessionTable',
37
+ Schema.StudioCMSSessionTable
38
+ );
39
+ export const StudioCMSPermissions = asDrizzleTable(
40
+ 'StudioCMSPermissions',
41
+ Schema.StudioCMSPermissions
42
+ );
43
+ export const StudioCMSDiffTracking = asDrizzleTable(
44
+ 'StudioCMSDiffTracking',
45
+ Schema.StudioCMSDiffTracking
46
+ );
47
+ export const StudioCMSPageContent = asDrizzleTable(
48
+ 'StudioCMSPageContent',
49
+ Schema.StudioCMSPageContent
50
+ );
51
+ export const StudioCMSEmailVerificationTokens = asDrizzleTable(
52
+ 'StudioCMSEmailVerificationTokens',
53
+ Schema.StudioCMSEmailVerificationTokens
54
+ );
package/src/env.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /// <reference path="../.astro/types.d.ts" />
2
+ /// <reference types="astro/client" />
3
+ /// <reference types="@studiocms/ui/v/types" />
@@ -0,0 +1,16 @@
1
+ /* Onest Variable Font from Fontsource */
2
+ @font-face {
3
+ font-family: "Onest Variable";
4
+ font-style: normal;
5
+ font-display: swap;
6
+ font-weight: 100 900;
7
+ src: url("../woff2/onest-variable.woff2") format("woff2-variations");
8
+ unicode-range:
9
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
10
+ }
11
+
12
+ /* CSS variable with Onest Variable font and system font fallback */
13
+ :root {
14
+ --scms-font-onest:
15
+ "Onest Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
16
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ import { Toaster } from 'studiocms:ui/components/toaster';
3
+ import PageHeader from '../components/PageHeader.astro';
4
+ import '../fonts/css/onest-variable.css';
5
+ import '../styles/global.css';
6
+ import 'studiocms:ui/prose';
7
+
8
+ interface Props {
9
+ title: string;
10
+ }
11
+
12
+ const { title } = Astro.props;
13
+ ---
14
+
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="utf-8" />
18
+ <meta name="viewport" content="width=device-width" />
19
+ <meta name="generator" content={Astro.generator} />
20
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
21
+ <title>{title}</title>
22
+ </head>
23
+ <body>
24
+ <Toaster />
25
+ <main>
26
+ <PageHeader />
27
+ <slot />
28
+ </main>
29
+ </body>
30
+ </html>
@@ -0,0 +1,88 @@
1
+ import type { z } from 'astro/zod';
2
+ import type {
3
+ booleanColumnSchema,
4
+ columnSchema,
5
+ columnsSchema,
6
+ dateColumnSchema,
7
+ dbConfigSchema,
8
+ indexSchema,
9
+ jsonColumnSchema,
10
+ MaybeArray,
11
+ numberColumnOptsSchema,
12
+ numberColumnSchema,
13
+ referenceableColumnSchema,
14
+ resolvedIndexSchema,
15
+ tableSchema,
16
+ textColumnOptsSchema,
17
+ textColumnSchema,
18
+ } from './schemas.js';
19
+
20
+ export type ResolvedIndexes = z.output<typeof dbConfigSchema>['tables'][string]['indexes'];
21
+ export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
22
+ export type BooleanColumnInput = z.input<typeof booleanColumnSchema>;
23
+ export type NumberColumn = z.infer<typeof numberColumnSchema>;
24
+ export type NumberColumnInput = z.input<typeof numberColumnSchema>;
25
+ export type TextColumn = z.infer<typeof textColumnSchema>;
26
+ export type TextColumnInput = z.input<typeof textColumnSchema>;
27
+ export type DateColumn = z.infer<typeof dateColumnSchema>;
28
+ export type DateColumnInput = z.input<typeof dateColumnSchema>;
29
+ export type JsonColumn = z.infer<typeof jsonColumnSchema>;
30
+ export type JsonColumnInput = z.input<typeof jsonColumnSchema>;
31
+
32
+ export type ColumnType =
33
+ | BooleanColumn['type']
34
+ | NumberColumn['type']
35
+ | TextColumn['type']
36
+ | DateColumn['type']
37
+ | JsonColumn['type'];
38
+
39
+ export type DBColumn = z.infer<typeof columnSchema>;
40
+ export type DBColumnInput =
41
+ | DateColumnInput
42
+ | BooleanColumnInput
43
+ | NumberColumnInput
44
+ | TextColumnInput
45
+ | JsonColumnInput;
46
+ export type DBColumns = z.infer<typeof columnsSchema>;
47
+ export type DBTable = z.infer<typeof tableSchema>;
48
+ export type DBTables = Record<string, DBTable>;
49
+ export type ResolvedDBTables = z.output<typeof dbConfigSchema>['tables'];
50
+ export type ResolvedDBTable = z.output<typeof dbConfigSchema>['tables'][string];
51
+ export type DBSnapshot = {
52
+ schema: Record<string, ResolvedDBTable>;
53
+ version: string;
54
+ };
55
+
56
+ export type DBConfigInput = z.input<typeof dbConfigSchema>;
57
+ export type DBConfig = z.infer<typeof dbConfigSchema>;
58
+
59
+ export type ColumnsConfig = z.input<typeof tableSchema>['columns'];
60
+ export type OutputColumnsConfig = z.output<typeof tableSchema>['columns'];
61
+
62
+ export interface TableConfig<TColumns extends ColumnsConfig = ColumnsConfig>
63
+ // use `extends` to ensure types line up with zod,
64
+ // only adding generics for type completions.
65
+ extends Pick<z.input<typeof tableSchema>, 'columns' | 'indexes' | 'foreignKeys'> {
66
+ columns: TColumns;
67
+ foreignKeys?: Array<{
68
+ columns: MaybeArray<Extract<keyof TColumns, string>>;
69
+ references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
70
+ }>;
71
+ indexes?: Array<IndexConfig<TColumns>> | Record<string, LegacyIndexConfig<TColumns>>;
72
+ deprecated?: boolean;
73
+ }
74
+
75
+ interface IndexConfig<TColumns extends ColumnsConfig> extends z.input<typeof indexSchema> {
76
+ on: MaybeArray<Extract<keyof TColumns, string>>;
77
+ }
78
+
79
+ /** @deprecated */
80
+ interface LegacyIndexConfig<TColumns extends ColumnsConfig>
81
+ extends z.input<typeof resolvedIndexSchema> {
82
+ on: MaybeArray<Extract<keyof TColumns, string>>;
83
+ }
84
+
85
+ // We cannot use `Omit<NumberColumn | TextColumn, 'type'>`,
86
+ // since Omit collapses our union type on primary key.
87
+ export type NumberColumnOpts = z.input<typeof numberColumnOptsSchema>;
88
+ export type TextColumnOpts = z.input<typeof textColumnOptsSchema>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * This is a modified version of Astro's error map. source:
3
+ * https://github.com/withastro/astro/blob/main/packages/astro/src/content/error-map.ts
4
+ */
5
+ /** biome-ignore-all lint/suspicious/noExplicitAny: source code from `@astrojs/db` */
6
+ import type { z } from 'astro/zod';
7
+
8
+ interface TypeOrLiteralErrByPathEntry {
9
+ code: 'invalid_type' | 'invalid_literal';
10
+ received: unknown;
11
+ expected: unknown[];
12
+ }
13
+
14
+ export const errorMap: z.ZodErrorMap = (baseError, ctx) => {
15
+ const baseErrorPath = flattenErrorPath(baseError.path);
16
+ if (baseError.code === 'invalid_union') {
17
+ // Optimization: Combine type and literal errors for keys that are common across ALL union types
18
+ // Ex. a union between `{ key: z.literal('tutorial') }` and `{ key: z.literal('blog') }` will
19
+ // raise a single error when `key` does not match:
20
+ // > Did not match union.
21
+ // > key: Expected `'tutorial' | 'blog'`, received 'foo'
22
+ const typeOrLiteralErrByPath = new Map<string, TypeOrLiteralErrByPathEntry>();
23
+ for (const unionError of baseError.unionErrors.flatMap((e) => e.errors)) {
24
+ if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') {
25
+ const flattenedErrorPath = flattenErrorPath(unionError.path);
26
+ const typeOrLiteralErr = typeOrLiteralErrByPath.get(flattenedErrorPath);
27
+ if (typeOrLiteralErr) {
28
+ typeOrLiteralErr.expected.push(unionError.expected);
29
+ } else {
30
+ typeOrLiteralErrByPath.set(flattenedErrorPath, {
31
+ code: unionError.code,
32
+ received: (unionError as any).received,
33
+ expected: [unionError.expected],
34
+ });
35
+ }
36
+ }
37
+ }
38
+ const messages: string[] = [
39
+ prefix(
40
+ baseErrorPath,
41
+ typeOrLiteralErrByPath.size ? 'Did not match union:' : 'Did not match union.'
42
+ ),
43
+ ];
44
+ return {
45
+ message: messages
46
+ .concat(
47
+ [...typeOrLiteralErrByPath.entries()]
48
+ // If type or literal error isn't common to ALL union types,
49
+ // filter it out. Can lead to confusing noise.
50
+ .filter(([, error]) => error.expected.length === baseError.unionErrors.length)
51
+ .map(([key, error]) =>
52
+ // Avoid printing the key again if it's a base error
53
+ key === baseErrorPath
54
+ ? `> ${getTypeOrLiteralMsg(error)}`
55
+ : `> ${prefix(key, getTypeOrLiteralMsg(error))}`
56
+ )
57
+ )
58
+ .join('\n'),
59
+ };
60
+ }
61
+ if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') {
62
+ return {
63
+ message: prefix(
64
+ baseErrorPath,
65
+ getTypeOrLiteralMsg({
66
+ code: baseError.code,
67
+ received: (baseError as any).received,
68
+ expected: [baseError.expected],
69
+ })
70
+ ),
71
+ };
72
+ }
73
+ if (baseError.message) {
74
+ return { message: prefix(baseErrorPath, baseError.message) };
75
+ }
76
+ return { message: prefix(baseErrorPath, ctx.defaultError) };
77
+ };
78
+
79
+ const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => {
80
+ if (error.received === 'undefined') return 'Required';
81
+ const expectedDeduped = new Set(error.expected);
82
+ switch (error.code) {
83
+ case 'invalid_type':
84
+ return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
85
+ error.received
86
+ )}`;
87
+ case 'invalid_literal':
88
+ return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
89
+ error.received
90
+ )}`;
91
+ }
92
+ };
93
+
94
+ const prefix = (key: string, msg: string) => (key.length ? `**${key}**: ${msg}` : msg);
95
+
96
+ const unionExpectedVals = (expectedVals: Set<unknown>) =>
97
+ [...expectedVals]
98
+ .map((expectedVal, idx) => {
99
+ if (idx === 0) return JSON.stringify(expectedVal);
100
+ const sep = ' | ';
101
+ return `${sep}${JSON.stringify(expectedVal)}`;
102
+ })
103
+ .join('');
104
+
105
+ const flattenErrorPath = (errorPath: Array<string | number>) => errorPath.join('.');
@@ -0,0 +1,149 @@
1
+ import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm';
2
+ import type { LibSQLDatabase } from 'drizzle-orm/libsql';
3
+ import {
4
+ customType,
5
+ type IndexBuilder,
6
+ index,
7
+ integer,
8
+ type SQLiteColumnBuilderBase,
9
+ sqliteTable,
10
+ text,
11
+ } from 'drizzle-orm/sqlite-core';
12
+ import type { ColumnsConfig, DBColumn, DBTable, TableConfig } from './core-types.js';
13
+ import { tableSchema } from './schemas.js';
14
+ import { isSerializedSQL, type SerializedSQL, type Table } from './types.js';
15
+ import { hasPrimaryKey } from './utils.js';
16
+ export type Database = LibSQLDatabase;
17
+ export type { Table } from './types.js';
18
+ export { hasPrimaryKey } from './utils.js';
19
+
20
+ // Taken from:
21
+ // https://stackoverflow.com/questions/52869695/check-if-a-date-string-is-in-iso-and-utc-format
22
+ const isISODateString = (str: string) => /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str);
23
+
24
+ const dateType = customType<{ data: Date; driverData: string }>({
25
+ dataType() {
26
+ return 'text';
27
+ },
28
+ toDriver(value) {
29
+ return value.toISOString();
30
+ },
31
+ fromDriver(value) {
32
+ if (!isISODateString(value)) {
33
+ // values saved using CURRENT_TIMESTAMP are not valid ISO strings
34
+ // but *are* in UTC, so append the UTC zone.
35
+ value += 'Z';
36
+ }
37
+ return new Date(value);
38
+ },
39
+ });
40
+
41
+ const jsonType = customType<{ data: unknown; driverData: string }>({
42
+ dataType() {
43
+ return 'text';
44
+ },
45
+ toDriver(value) {
46
+ return JSON.stringify(value);
47
+ },
48
+ fromDriver(value) {
49
+ return JSON.parse(value);
50
+ },
51
+ });
52
+
53
+ type D1ColumnBuilder = SQLiteColumnBuilderBase<
54
+ ColumnBuilderBaseConfig<ColumnDataType, string> & { data: unknown }
55
+ >;
56
+
57
+ export function internal_asDrizzleTable(name: string, table: DBTable) {
58
+ const columns: Record<string, D1ColumnBuilder> = {};
59
+ if (!Object.entries(table.columns).some(([, column]) => hasPrimaryKey(column))) {
60
+ columns._id = integer('_id').primaryKey();
61
+ }
62
+ for (const [columnName, column] of Object.entries(table.columns)) {
63
+ columns[columnName] = columnMapper(columnName, column);
64
+ }
65
+ const drizzleTable = sqliteTable(name, columns, (ormTable) => {
66
+ const indexes: Array<IndexBuilder> = [];
67
+ for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) {
68
+ const onColNames = Array.isArray(indexProps.on) ? indexProps.on : [indexProps.on];
69
+ const onCols = onColNames.map((colName) => ormTable[colName]);
70
+ if (!atLeastOne(onCols)) continue;
71
+
72
+ indexes.push(index(indexName).on(...onCols));
73
+ }
74
+ return indexes;
75
+ });
76
+ return drizzleTable;
77
+ }
78
+
79
+ export function asDrizzleTable<
80
+ TableName extends string = string,
81
+ TColumns extends ColumnsConfig = ColumnsConfig,
82
+ >(name: TableName, tableConfig: TableConfig<TColumns>) {
83
+ return internal_asDrizzleTable(name, tableSchema.parse(tableConfig)) as Table<
84
+ TableName,
85
+ TColumns
86
+ >;
87
+ }
88
+
89
+ function atLeastOne<T>(arr: T[]): arr is [T, ...T[]] {
90
+ return arr.length > 0;
91
+ }
92
+
93
+ function columnMapper(columnName: string, column: DBColumn) {
94
+ let c: ReturnType<
95
+ | typeof text
96
+ | typeof integer
97
+ | typeof jsonType
98
+ | typeof dateType
99
+ | typeof integer<string, 'boolean'>
100
+ >;
101
+
102
+ switch (column.type) {
103
+ case 'text': {
104
+ c = text(columnName, { enum: column.schema.enum });
105
+ // Duplicate default logic across cases to preserve type inference.
106
+ // No clean generic for every column builder.
107
+ if (column.schema.default !== undefined)
108
+ c = c.default(handleSerializedSQL(column.schema.default));
109
+ if (column.schema.primaryKey === true) c = c.primaryKey();
110
+ break;
111
+ }
112
+ case 'number': {
113
+ c = integer(columnName);
114
+ if (column.schema.default !== undefined)
115
+ c = c.default(handleSerializedSQL(column.schema.default));
116
+ if (column.schema.primaryKey === true) c = c.primaryKey();
117
+ break;
118
+ }
119
+ case 'boolean': {
120
+ c = integer(columnName, { mode: 'boolean' });
121
+ if (column.schema.default !== undefined)
122
+ c = c.default(handleSerializedSQL(column.schema.default));
123
+ break;
124
+ }
125
+ case 'json':
126
+ c = jsonType(columnName);
127
+ if (column.schema.default !== undefined) c = c.default(column.schema.default);
128
+ break;
129
+ case 'date': {
130
+ c = dateType(columnName);
131
+ if (column.schema.default !== undefined) {
132
+ const def = handleSerializedSQL(column.schema.default);
133
+ c = c.default(typeof def === 'string' ? new Date(def) : def);
134
+ }
135
+ break;
136
+ }
137
+ }
138
+
139
+ if (!column.schema.optional) c = c.notNull();
140
+ if (column.schema.unique) c = c.unique();
141
+ return c;
142
+ }
143
+
144
+ function handleSerializedSQL<T>(def: T | SerializedSQL) {
145
+ if (isSerializedSQL(def)) {
146
+ return sql.raw(def.sql);
147
+ }
148
+ return def;
149
+ }