appflare 0.0.28 → 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.
- package/cli/commands/index.ts +140 -0
- package/cli/generate.ts +149 -0
- package/cli/index.ts +56 -447
- package/cli/load-config.ts +182 -0
- package/cli/schema-compiler.ts +657 -0
- package/cli/templates/auth/README.md +156 -0
- package/cli/templates/auth/config.ts +61 -0
- package/cli/templates/auth/route-config.ts +18 -0
- package/cli/templates/auth/route-handler.ts +18 -0
- package/cli/templates/auth/route-request-utils.ts +55 -0
- package/cli/templates/auth/route.ts +14 -0
- package/cli/templates/core/README.md +266 -0
- package/cli/templates/core/app-creation.ts +19 -0
- package/cli/templates/core/client/appflare.ts +37 -0
- package/cli/templates/core/client/index.ts +6 -0
- package/cli/templates/core/client/storage.ts +100 -0
- package/cli/templates/core/client/types.ts +54 -0
- package/cli/templates/core/client-modules/appflare.ts +112 -0
- package/cli/templates/core/client-modules/handlers/index.ts +740 -0
- package/cli/templates/core/client-modules/handlers.ts +1 -0
- package/cli/templates/core/client-modules/index.ts +7 -0
- package/cli/templates/core/client-modules/storage.ts +180 -0
- package/cli/templates/core/client-modules/types.ts +145 -0
- package/cli/templates/core/client.ts +39 -0
- package/cli/templates/core/drizzle.ts +15 -0
- package/cli/templates/core/export.ts +14 -0
- package/cli/templates/core/handlers-route.ts +23 -0
- package/cli/templates/core/handlers.ts +1 -0
- package/cli/templates/core/imports.ts +8 -0
- package/cli/templates/core/server.ts +38 -0
- package/cli/templates/core/types.ts +6 -0
- package/cli/templates/core/wrangler.ts +109 -0
- package/cli/templates/handlers/README.md +265 -0
- package/cli/templates/handlers/auth.ts +36 -0
- package/cli/templates/handlers/execution.ts +39 -0
- package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
- package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
- package/cli/templates/handlers/generators/context/types.ts +18 -0
- package/cli/templates/handlers/generators/context.ts +43 -0
- package/cli/templates/handlers/generators/execution.ts +15 -0
- package/cli/templates/handlers/generators/handlers.ts +13 -0
- package/cli/templates/handlers/index.ts +43 -0
- package/cli/templates/handlers/operations.ts +116 -0
- package/cli/templates/handlers/registration.ts +1114 -0
- package/cli/templates/handlers/types.ts +960 -0
- package/cli/templates/handlers/utils.ts +48 -0
- package/cli/types.ts +108 -0
- package/cli/utils/handler-discovery.ts +366 -0
- package/cli/utils/json-utils.ts +24 -0
- package/cli/utils/path-utils.ts +19 -0
- package/cli/utils/schema-discovery.ts +390 -0
- package/index.ts +27 -4
- package/package.json +23 -20
- package/react/index.ts +5 -3
- package/react/use-infinite-query.ts +190 -0
- package/react/use-mutation.ts +54 -0
- package/react/use-query.ts +158 -0
- package/schema.ts +262 -0
- package/tsconfig.json +2 -4
- package/cli/README.md +0 -108
- package/cli/core/build.ts +0 -187
- package/cli/core/config.ts +0 -92
- package/cli/core/discover-handlers.ts +0 -143
- package/cli/core/handlers.ts +0 -7
- package/cli/core/index.ts +0 -205
- package/cli/generators/generate-api-client/client.ts +0 -163
- package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
- package/cli/generators/generate-api-client/index.ts +0 -973
- package/cli/generators/generate-api-client/types.ts +0 -164
- package/cli/generators/generate-api-client/utils.ts +0 -22
- package/cli/generators/generate-api-client.ts +0 -1
- package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
- package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
- package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
- package/cli/generators/generate-cloudflare-worker.ts +0 -4
- package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
- package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
- package/cli/generators/generate-cron-handlers/index.ts +0 -61
- package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
- package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
- package/cli/generators/generate-db-handlers/index.ts +0 -33
- package/cli/generators/generate-db-handlers/prepare.ts +0 -24
- package/cli/generators/generate-db-handlers/templates.ts +0 -189
- package/cli/generators/generate-db-handlers.ts +0 -1
- package/cli/generators/generate-hono-server/auth.ts +0 -97
- package/cli/generators/generate-hono-server/imports.ts +0 -55
- package/cli/generators/generate-hono-server/index.ts +0 -52
- package/cli/generators/generate-hono-server/routes.ts +0 -115
- package/cli/generators/generate-hono-server/template.ts +0 -371
- package/cli/generators/generate-hono-server.ts +0 -1
- package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
- package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
- package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
- package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
- package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers.ts +0 -1
- package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
- package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
- package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
- package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
- package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
- package/cli/generators/generate-websocket-durable-object.ts +0 -1
- package/cli/schema/schema-static-types.ts +0 -702
- package/cli/schema/schema.ts +0 -151
- package/cli/utils/tsc.ts +0 -54
- package/cli/utils/utils.ts +0 -190
- package/cli/utils/zod-utils.ts +0 -121
- package/lib/README.md +0 -50
- package/lib/db.ts +0 -19
- package/lib/location.ts +0 -110
- package/lib/values.ts +0 -27
- package/react/README.md +0 -67
- package/react/hooks/useMutation.ts +0 -89
- package/react/hooks/usePaginatedQuery.ts +0 -213
- package/react/hooks/useQuery.ts +0 -106
- package/react/shared/queryShared.ts +0 -174
- package/server/README.md +0 -218
- package/server/auth.ts +0 -107
- package/server/database/builders.ts +0 -83
- package/server/database/context.ts +0 -327
- package/server/database/populate.ts +0 -234
- package/server/database/query-builder.ts +0 -161
- package/server/database/query-utils.ts +0 -25
- package/server/db.ts +0 -2
- package/server/storage/auth.ts +0 -16
- package/server/storage/bucket.ts +0 -22
- package/server/storage/context.ts +0 -34
- package/server/storage/index.ts +0 -38
- package/server/storage/operations.ts +0 -149
- package/server/storage/route-handler.ts +0 -60
- package/server/storage/types.ts +0 -55
- package/server/storage/utils.ts +0 -47
- package/server/storage.ts +0 -6
- package/server/types/schema-refs.ts +0 -66
- package/server/types/types.ts +0 -633
- package/server/utils/id-utils.ts +0 -230
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type {
|
|
5
|
+
ColumnDefinition,
|
|
6
|
+
ColumnType,
|
|
7
|
+
ManyRelationDefinition,
|
|
8
|
+
OneRelationDefinition,
|
|
9
|
+
SchemaDefinition,
|
|
10
|
+
TableDefinition,
|
|
11
|
+
} from "../schema";
|
|
12
|
+
import { isSchemaDefinition } from "../schema";
|
|
13
|
+
import type { LoadedAppflareConfig } from "./types";
|
|
14
|
+
|
|
15
|
+
export type CompiledSchemaArtifacts = {
|
|
16
|
+
schemaPath: string;
|
|
17
|
+
typesPath: string;
|
|
18
|
+
zodPath: string;
|
|
19
|
+
tableNames: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function toSnakeCase(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
25
|
+
.replace(/[\s-]+/g, "_")
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toPascalCase(value: string): string {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/[_-]+/g, " ")
|
|
32
|
+
.replace(/\s+(.)/g, (_match, char: string) => char.toUpperCase())
|
|
33
|
+
.replace(/\s/g, "")
|
|
34
|
+
.replace(/^(.)/, (_match, char: string) => char.toUpperCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function singularize(value: string): string {
|
|
38
|
+
if (value.endsWith("ies")) {
|
|
39
|
+
return `${value.slice(0, -3)}y`;
|
|
40
|
+
}
|
|
41
|
+
if (value.endsWith("ses")) {
|
|
42
|
+
return value.slice(0, -2);
|
|
43
|
+
}
|
|
44
|
+
if (value.endsWith("s") && value.length > 1) {
|
|
45
|
+
return value.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function quote(value: string): string {
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toSqlLiteral(value: unknown): string {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
return quote(value);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
if (value === null) {
|
|
62
|
+
return "null";
|
|
63
|
+
}
|
|
64
|
+
if (value instanceof Date) {
|
|
65
|
+
return quote(value.toISOString());
|
|
66
|
+
}
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unsupported SQL default value '${String(value)}'. Use string, number, boolean, null, or Date.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cloneSchemaDefinition(definition: SchemaDefinition): SchemaDefinition {
|
|
73
|
+
return {
|
|
74
|
+
kind: "schema",
|
|
75
|
+
tables: Object.fromEntries(
|
|
76
|
+
Object.entries(definition.tables).map(([tableName, table]) => {
|
|
77
|
+
return [
|
|
78
|
+
tableName,
|
|
79
|
+
{
|
|
80
|
+
kind: "table",
|
|
81
|
+
sqlName: table.sqlName,
|
|
82
|
+
columns: Object.fromEntries(
|
|
83
|
+
Object.entries(table.columns).map(([columnName, column]) => {
|
|
84
|
+
return [columnName, { ...column }];
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
relations: Object.fromEntries(
|
|
88
|
+
Object.entries(table.relations).map(
|
|
89
|
+
([relationName, relation]) => {
|
|
90
|
+
return [relationName, { ...relation }];
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
} satisfies TableDefinition,
|
|
95
|
+
];
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getLocalReferenceType(
|
|
102
|
+
definition: SchemaDefinition,
|
|
103
|
+
targetTable: string,
|
|
104
|
+
referenceField: string,
|
|
105
|
+
): ColumnType | undefined {
|
|
106
|
+
const table = definition.tables[targetTable];
|
|
107
|
+
if (!table) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const targetColumn = table.columns[referenceField];
|
|
111
|
+
return targetColumn?.type;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureInferredReferenceColumn(
|
|
115
|
+
tableName: string,
|
|
116
|
+
table: TableDefinition,
|
|
117
|
+
columnName: string,
|
|
118
|
+
referenceTable: string,
|
|
119
|
+
referenceField: string,
|
|
120
|
+
options: {
|
|
121
|
+
fkType?: ColumnType;
|
|
122
|
+
sqlName?: string;
|
|
123
|
+
notNull?: boolean;
|
|
124
|
+
},
|
|
125
|
+
inferredType: ColumnType,
|
|
126
|
+
): void {
|
|
127
|
+
const existing = table.columns[columnName];
|
|
128
|
+
if (existing) {
|
|
129
|
+
if (
|
|
130
|
+
existing.references &&
|
|
131
|
+
(existing.references.table !== referenceTable ||
|
|
132
|
+
existing.references.column !== referenceField)
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Inferred relation '${tableName}.${columnName}' conflicts with explicit references(${existing.references.table}.${existing.references.column}).`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
table.columns[columnName] = {
|
|
140
|
+
...existing,
|
|
141
|
+
references: { table: referenceTable, column: referenceField },
|
|
142
|
+
};
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
table.columns[columnName] = {
|
|
147
|
+
kind: "column",
|
|
148
|
+
type: options.fkType ?? inferredType,
|
|
149
|
+
sqlName: options.sqlName,
|
|
150
|
+
notNull: options.notNull ?? true,
|
|
151
|
+
references: {
|
|
152
|
+
table: referenceTable,
|
|
153
|
+
column: referenceField,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeSchemaDefinition(
|
|
159
|
+
definition: SchemaDefinition,
|
|
160
|
+
): SchemaDefinition {
|
|
161
|
+
const normalized = cloneSchemaDefinition(definition);
|
|
162
|
+
|
|
163
|
+
for (const [tableName, table] of Object.entries(normalized.tables)) {
|
|
164
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
165
|
+
if (relation.relation !== "one") {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const referenceField = relation.referenceField ?? "id";
|
|
170
|
+
const inferredField = relation.field ?? `${relationName}Id`;
|
|
171
|
+
relation.field = inferredField;
|
|
172
|
+
|
|
173
|
+
const inferredType =
|
|
174
|
+
getLocalReferenceType(
|
|
175
|
+
normalized,
|
|
176
|
+
relation.targetTable,
|
|
177
|
+
referenceField,
|
|
178
|
+
) ??
|
|
179
|
+
relation.fkType ??
|
|
180
|
+
"string";
|
|
181
|
+
|
|
182
|
+
ensureInferredReferenceColumn(
|
|
183
|
+
tableName,
|
|
184
|
+
table,
|
|
185
|
+
inferredField,
|
|
186
|
+
relation.targetTable,
|
|
187
|
+
referenceField,
|
|
188
|
+
{
|
|
189
|
+
fkType: relation.fkType,
|
|
190
|
+
sqlName: relation.sqlName,
|
|
191
|
+
notNull: relation.notNull,
|
|
192
|
+
},
|
|
193
|
+
inferredType,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const [sourceTableName, sourceTable] of Object.entries(
|
|
199
|
+
normalized.tables,
|
|
200
|
+
)) {
|
|
201
|
+
for (const relation of Object.values(sourceTable.relations)) {
|
|
202
|
+
if (relation.relation !== "many") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const targetTable = normalized.tables[relation.targetTable];
|
|
207
|
+
if (!targetTable) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const referenceField = relation.referenceField ?? "id";
|
|
212
|
+
const inferredField =
|
|
213
|
+
relation.field ?? `${singularize(sourceTableName)}Id`;
|
|
214
|
+
const inferredType =
|
|
215
|
+
getLocalReferenceType(normalized, sourceTableName, referenceField) ??
|
|
216
|
+
relation.fkType ??
|
|
217
|
+
"string";
|
|
218
|
+
|
|
219
|
+
ensureInferredReferenceColumn(
|
|
220
|
+
relation.targetTable,
|
|
221
|
+
targetTable,
|
|
222
|
+
inferredField,
|
|
223
|
+
sourceTableName,
|
|
224
|
+
referenceField,
|
|
225
|
+
{
|
|
226
|
+
fkType: relation.fkType,
|
|
227
|
+
sqlName: relation.sqlName,
|
|
228
|
+
notNull: relation.notNull,
|
|
229
|
+
},
|
|
230
|
+
inferredType,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return normalized;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isOptionalInsertColumn(column: ColumnDefinition): boolean {
|
|
239
|
+
return (
|
|
240
|
+
column.notNull !== true ||
|
|
241
|
+
column.autoIncrement === true ||
|
|
242
|
+
column.sqlDefault !== undefined ||
|
|
243
|
+
column.runtimeDefaultFn !== undefined
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isNullableSelectColumn(column: ColumnDefinition): boolean {
|
|
248
|
+
return column.notNull !== true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function drizzleBaseColumn(
|
|
252
|
+
fieldName: string,
|
|
253
|
+
column: ColumnDefinition,
|
|
254
|
+
strategy: "camelToSnake",
|
|
255
|
+
): string {
|
|
256
|
+
const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
|
|
257
|
+
const needsExplicitName = resolvedSqlName !== fieldName;
|
|
258
|
+
|
|
259
|
+
if (column.type === "int") {
|
|
260
|
+
return needsExplicitName ? `t.int(${quote(resolvedSqlName)})` : "t.int()";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (column.type === "string") {
|
|
264
|
+
if (column.length !== undefined) {
|
|
265
|
+
if (needsExplicitName) {
|
|
266
|
+
return `t.text(${quote(resolvedSqlName)}, { length: ${column.length} })`;
|
|
267
|
+
}
|
|
268
|
+
return `t.text({ length: ${column.length} })`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (column.type === "boolean") {
|
|
275
|
+
if (needsExplicitName) {
|
|
276
|
+
return `t.int(${quote(resolvedSqlName)}, { mode: "boolean" })`;
|
|
277
|
+
}
|
|
278
|
+
return 't.int({ mode: "boolean" })';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (column.type === "date") {
|
|
282
|
+
if (needsExplicitName) {
|
|
283
|
+
return `t.int(${quote(resolvedSqlName)}, { mode: "timestamp_ms" })`;
|
|
284
|
+
}
|
|
285
|
+
return 't.int({ mode: "timestamp_ms" })';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (strategy === "camelToSnake") {
|
|
289
|
+
return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return "t.text()";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resolveOneRelationReference(
|
|
296
|
+
tableName: string,
|
|
297
|
+
table: TableDefinition,
|
|
298
|
+
relation: OneRelationDefinition,
|
|
299
|
+
): { sourceField: string; targetField: string } {
|
|
300
|
+
const sourceField = relation.field;
|
|
301
|
+
const targetField = relation.referenceField ?? "id";
|
|
302
|
+
|
|
303
|
+
if (!sourceField) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Relation on '${tableName}' targeting '${relation.targetTable}' is missing a local field.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!(sourceField in table.columns)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Relation '${tableName}.${sourceField}' references missing local field '${sourceField}'.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
sourceField,
|
|
317
|
+
targetField,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildExternalTableImportLines(externals: Set<string>): string {
|
|
322
|
+
if (externals.size === 0) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
return `import { ${Array.from(externals).sort().join(", ")} } from \"./auth.schema\";\n`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getRelationImports(table: TableDefinition): string[] {
|
|
329
|
+
return Object.values(table.relations)
|
|
330
|
+
.filter((relation) => relation.relation === "one")
|
|
331
|
+
.map((relation) => relation.targetTable);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function resolveColumnReference(
|
|
335
|
+
fieldName: string,
|
|
336
|
+
column: ColumnDefinition,
|
|
337
|
+
table: TableDefinition,
|
|
338
|
+
): { tableName: string; fieldName: string } | undefined {
|
|
339
|
+
if (column.references) {
|
|
340
|
+
return {
|
|
341
|
+
tableName: column.references.table,
|
|
342
|
+
fieldName: column.references.column,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const matchingOne = Object.values(table.relations).find((relation) => {
|
|
347
|
+
return relation.relation === "one" && relation.field === fieldName;
|
|
348
|
+
}) as OneRelationDefinition | undefined;
|
|
349
|
+
|
|
350
|
+
if (!matchingOne) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
tableName: matchingOne.targetTable,
|
|
356
|
+
fieldName: matchingOne.referenceField ?? "id",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function emitDrizzleSchema(
|
|
361
|
+
definition: SchemaDefinition,
|
|
362
|
+
strategy: "camelToSnake",
|
|
363
|
+
): string {
|
|
364
|
+
const localTables = new Set(Object.keys(definition.tables));
|
|
365
|
+
const externalTables = new Set<string>();
|
|
366
|
+
|
|
367
|
+
for (const table of Object.values(definition.tables)) {
|
|
368
|
+
for (const relationTarget of getRelationImports(table)) {
|
|
369
|
+
if (!localTables.has(relationTarget)) {
|
|
370
|
+
externalTables.add(relationTarget);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const column of Object.values(table.columns)) {
|
|
374
|
+
if (column.references && !localTables.has(column.references.table)) {
|
|
375
|
+
externalTables.add(column.references.table);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const tableBlocks: string[] = [];
|
|
381
|
+
const relationBlocks: string[] = [];
|
|
382
|
+
|
|
383
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
384
|
+
const sqlTableName = table.sqlName ?? toSnakeCase(tableName);
|
|
385
|
+
const columnLines: string[] = [];
|
|
386
|
+
const indexes: string[] = [];
|
|
387
|
+
|
|
388
|
+
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
389
|
+
let expr = drizzleBaseColumn(fieldName, column, strategy);
|
|
390
|
+
|
|
391
|
+
if (column.primaryKey) {
|
|
392
|
+
expr += column.autoIncrement
|
|
393
|
+
? ".primaryKey({ autoIncrement: true })"
|
|
394
|
+
: ".primaryKey()";
|
|
395
|
+
}
|
|
396
|
+
if (column.notNull) {
|
|
397
|
+
expr += ".notNull()";
|
|
398
|
+
}
|
|
399
|
+
if (column.sqlDefault !== undefined) {
|
|
400
|
+
expr += `.default(${toSqlLiteral(column.sqlDefault)})`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const reference = resolveColumnReference(fieldName, column, table);
|
|
404
|
+
if (reference) {
|
|
405
|
+
expr += `.references(() => ${reference.tableName}.${reference.fieldName})`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (column.unique) {
|
|
409
|
+
const uniqueName =
|
|
410
|
+
typeof column.unique === "object" && column.unique.name
|
|
411
|
+
? column.unique.name
|
|
412
|
+
: `${sqlTableName}_${toSnakeCase(fieldName)}_unique_idx`;
|
|
413
|
+
indexes.push(
|
|
414
|
+
`\t\tt.uniqueIndex(${quote(uniqueName)}).on(table.${fieldName})`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (column.index) {
|
|
419
|
+
const indexName =
|
|
420
|
+
typeof column.index === "object" && column.index.name
|
|
421
|
+
? column.index.name
|
|
422
|
+
: `${sqlTableName}_${toSnakeCase(fieldName)}_idx`;
|
|
423
|
+
indexes.push(`\t\tt.index(${quote(indexName)}).on(table.${fieldName})`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
columnLines.push(`\t\t${fieldName}: ${expr},`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (indexes.length > 0) {
|
|
430
|
+
tableBlocks.push(
|
|
431
|
+
`export const ${tableName} = table(\n\t${quote(sqlTableName)},\n\t{\n${columnLines.join("\n")}\n\t},\n\t(table) => [\n${indexes.join(",\n")}\n\t],\n);`,
|
|
432
|
+
);
|
|
433
|
+
} else {
|
|
434
|
+
tableBlocks.push(
|
|
435
|
+
`export const ${tableName} = table(${quote(sqlTableName)}, {\n${columnLines.join("\n")}\n\t});`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const oneRelations = Object.entries(table.relations).filter(
|
|
440
|
+
([, relation]) => {
|
|
441
|
+
return relation.relation === "one";
|
|
442
|
+
},
|
|
443
|
+
) as Array<[string, OneRelationDefinition]>;
|
|
444
|
+
const manyRelations = Object.entries(table.relations).filter(
|
|
445
|
+
([, relation]) => {
|
|
446
|
+
return relation.relation === "many";
|
|
447
|
+
},
|
|
448
|
+
) as Array<[string, ManyRelationDefinition]>;
|
|
449
|
+
|
|
450
|
+
if (oneRelations.length === 0 && manyRelations.length === 0) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const relationLines: string[] = [];
|
|
455
|
+
for (const [relationName, relation] of oneRelations) {
|
|
456
|
+
const resolved = resolveOneRelationReference(tableName, table, relation);
|
|
457
|
+
relationLines.push(
|
|
458
|
+
`\t${relationName}: one(${relation.targetTable}, {\n\t\tfields: [${tableName}.${resolved.sourceField}],\n\t\treferences: [${relation.targetTable}.${resolved.targetField}],\n\t}),`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
for (const [relationName, relation] of manyRelations) {
|
|
462
|
+
relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
relationBlocks.push(
|
|
466
|
+
`export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return `import * as t from "drizzle-orm/sqlite-core";
|
|
471
|
+
import { sqliteTable as table } from "drizzle-orm/sqlite-core";
|
|
472
|
+
import { relations } from "drizzle-orm";
|
|
473
|
+
${buildExternalTableImportLines(externalTables)}
|
|
474
|
+
${tableBlocks.join("\n\n")}
|
|
475
|
+
|
|
476
|
+
${relationBlocks.join("\n\n")}
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function zodSchemaExpression(
|
|
481
|
+
column: ColumnDefinition,
|
|
482
|
+
optional: boolean,
|
|
483
|
+
): string {
|
|
484
|
+
let expr = "z.unknown()";
|
|
485
|
+
if (column.type === "int") {
|
|
486
|
+
expr = "z.number().int()";
|
|
487
|
+
} else if (column.type === "string") {
|
|
488
|
+
expr = "z.string()";
|
|
489
|
+
if (column.length !== undefined) {
|
|
490
|
+
expr += `.max(${column.length})`;
|
|
491
|
+
}
|
|
492
|
+
} else if (column.type === "boolean") {
|
|
493
|
+
expr = "z.boolean()";
|
|
494
|
+
} else if (column.type === "date") {
|
|
495
|
+
expr = "z.date()";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (optional) {
|
|
499
|
+
expr += ".optional()";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return expr;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function emitZodSchemas(definition: SchemaDefinition): string {
|
|
506
|
+
const blocks: string[] = [];
|
|
507
|
+
|
|
508
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
509
|
+
const pascal = toPascalCase(tableName);
|
|
510
|
+
const insertLines: string[] = [];
|
|
511
|
+
const selectLines: string[] = [];
|
|
512
|
+
|
|
513
|
+
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
514
|
+
insertLines.push(
|
|
515
|
+
`\t${fieldName}: ${zodSchemaExpression(column, isOptionalInsertColumn(column))},`,
|
|
516
|
+
);
|
|
517
|
+
selectLines.push(
|
|
518
|
+
`\t${fieldName}: ${zodSchemaExpression(column, isNullableSelectColumn(column))},`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
blocks.push(`export const ${tableName}InsertSchema = z.object({\n${insertLines.join("\n")}\n});
|
|
523
|
+
export const ${tableName}SelectSchema = z.object({\n${selectLines.join("\n")}\n});
|
|
524
|
+
|
|
525
|
+
export type ${pascal}Insert = z.infer<typeof ${tableName}InsertSchema>;
|
|
526
|
+
export type ${pascal}Select = z.infer<typeof ${tableName}SelectSchema>;
|
|
527
|
+
`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return `import { z } from "zod";
|
|
531
|
+
|
|
532
|
+
${blocks.join("\n")}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function toTypeScriptType(column: ColumnDefinition): string {
|
|
536
|
+
if (column.type === "int") {
|
|
537
|
+
return "number";
|
|
538
|
+
}
|
|
539
|
+
if (column.type === "string") {
|
|
540
|
+
return "string";
|
|
541
|
+
}
|
|
542
|
+
if (column.type === "boolean") {
|
|
543
|
+
return "boolean";
|
|
544
|
+
}
|
|
545
|
+
if (column.type === "date") {
|
|
546
|
+
return "Date";
|
|
547
|
+
}
|
|
548
|
+
return "unknown";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function emitTypes(definition: SchemaDefinition): string {
|
|
552
|
+
const lines: string[] = [];
|
|
553
|
+
|
|
554
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
555
|
+
const pascal = toPascalCase(tableName);
|
|
556
|
+
const selectFields: string[] = [];
|
|
557
|
+
const insertFields: string[] = [];
|
|
558
|
+
|
|
559
|
+
for (const [fieldName, column] of Object.entries(table.columns)) {
|
|
560
|
+
const tsType = toTypeScriptType(column);
|
|
561
|
+
selectFields.push(
|
|
562
|
+
`\t${fieldName}${isNullableSelectColumn(column) ? "?" : ""}: ${tsType};`,
|
|
563
|
+
);
|
|
564
|
+
insertFields.push(
|
|
565
|
+
`\t${fieldName}${isOptionalInsertColumn(column) ? "?" : ""}: ${tsType};`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
lines.push(`export type ${pascal} = {\n${selectFields.join("\n")}\n};
|
|
570
|
+
|
|
571
|
+
export type New${pascal} = {\n${insertFields.join("\n")}\n};`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return `${lines.join("\n\n")}
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function findSchemaExport(
|
|
579
|
+
moduleExports: Record<string, unknown>,
|
|
580
|
+
exportName?: string,
|
|
581
|
+
): SchemaDefinition {
|
|
582
|
+
if (exportName) {
|
|
583
|
+
const explicit = moduleExports[exportName];
|
|
584
|
+
if (!isSchemaDefinition(explicit)) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`schemaDsl.exportName '${exportName}' does not point to a schema() export.`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return explicit;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const candidate of Object.values(moduleExports)) {
|
|
593
|
+
if (isSchemaDefinition(candidate)) {
|
|
594
|
+
return candidate;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
throw new Error(
|
|
599
|
+
"No schema() export found in schemaDsl entry module. Set schemaDsl.exportName to the correct export.",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function compileSchemaDsl(
|
|
604
|
+
loadedConfig: LoadedAppflareConfig,
|
|
605
|
+
): Promise<CompiledSchemaArtifacts | undefined> {
|
|
606
|
+
const schemaDsl = loadedConfig.config.schemaDsl;
|
|
607
|
+
if (!schemaDsl) {
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const namingStrategy = schemaDsl.namingStrategy ?? "camelToSnake";
|
|
612
|
+
const entryPath = resolve(loadedConfig.configDir, schemaDsl.entry);
|
|
613
|
+
const outSchemaPath = resolve(
|
|
614
|
+
loadedConfig.configDir,
|
|
615
|
+
schemaDsl.outFile ?? resolve(loadedConfig.outDirAbs, "schema.compiled.ts"),
|
|
616
|
+
);
|
|
617
|
+
const outTypesPath = resolve(
|
|
618
|
+
loadedConfig.configDir,
|
|
619
|
+
schemaDsl.typesOutFile ??
|
|
620
|
+
resolve(loadedConfig.outDirAbs, "schema.types.ts"),
|
|
621
|
+
);
|
|
622
|
+
const outZodPath = resolve(
|
|
623
|
+
loadedConfig.configDir,
|
|
624
|
+
schemaDsl.zodOutFile ?? resolve(loadedConfig.outDirAbs, "schema.zod.ts"),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const moduleUrl = `${pathToFileURL(entryPath).href}?t=${Date.now()}`;
|
|
628
|
+
const schemaModule = (await import(moduleUrl)) as Record<string, unknown>;
|
|
629
|
+
const schemaDefinition = findSchemaExport(schemaModule, schemaDsl.exportName);
|
|
630
|
+
const normalizedSchema = normalizeSchemaDefinition(schemaDefinition);
|
|
631
|
+
|
|
632
|
+
await Promise.all([
|
|
633
|
+
mkdir(dirname(outSchemaPath), { recursive: true }),
|
|
634
|
+
mkdir(dirname(outTypesPath), { recursive: true }),
|
|
635
|
+
mkdir(dirname(outZodPath), { recursive: true }),
|
|
636
|
+
]);
|
|
637
|
+
|
|
638
|
+
const drizzleSchemaSource = emitDrizzleSchema(
|
|
639
|
+
normalizedSchema,
|
|
640
|
+
namingStrategy,
|
|
641
|
+
);
|
|
642
|
+
const typesSource = emitTypes(normalizedSchema);
|
|
643
|
+
const zodSource = emitZodSchemas(normalizedSchema);
|
|
644
|
+
|
|
645
|
+
await Promise.all([
|
|
646
|
+
Bun.write(outSchemaPath, drizzleSchemaSource),
|
|
647
|
+
Bun.write(outTypesPath, typesSource),
|
|
648
|
+
Bun.write(outZodPath, zodSource),
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
schemaPath: outSchemaPath,
|
|
653
|
+
typesPath: outTypesPath,
|
|
654
|
+
zodPath: outZodPath,
|
|
655
|
+
tableNames: Object.keys(normalizedSchema.tables),
|
|
656
|
+
};
|
|
657
|
+
}
|