@pol-studios/powersync 1.0.10 → 1.0.12
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/dist/generator/cli.js +719 -0
- package/dist/generator/index.d.ts +257 -0
- package/dist/generator/index.js +590 -0
- package/dist/generator/index.js.map +1 -0
- package/package.json +12 -2
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/generator/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import * as fs2 from "fs";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
|
|
9
|
+
// src/generator/generator.ts
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
|
|
13
|
+
// src/generator/config.ts
|
|
14
|
+
var DEFAULT_SKIP_COLUMNS = [
|
|
15
|
+
"id",
|
|
16
|
+
// PowerSync handles id automatically
|
|
17
|
+
// Legacy numeric ID columns - typically not needed after UUID migration
|
|
18
|
+
"legacyId"
|
|
19
|
+
];
|
|
20
|
+
var DEFAULT_DECIMAL_PATTERNS = [
|
|
21
|
+
"hours",
|
|
22
|
+
"watts",
|
|
23
|
+
"voltage",
|
|
24
|
+
"rate",
|
|
25
|
+
"amount",
|
|
26
|
+
"price",
|
|
27
|
+
"cost",
|
|
28
|
+
"total"
|
|
29
|
+
];
|
|
30
|
+
var DEFAULT_INDEX_PATTERNS = [
|
|
31
|
+
"Id$",
|
|
32
|
+
// FK columns ending in Id (e.g., projectId, userId)
|
|
33
|
+
"^createdAt$",
|
|
34
|
+
"^updatedAt$",
|
|
35
|
+
"^status$",
|
|
36
|
+
"^type$"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// src/generator/parser.ts
|
|
40
|
+
function parseRowType(tableContent, options) {
|
|
41
|
+
const columns = /* @__PURE__ */ new Map();
|
|
42
|
+
const rowMatch = tableContent.match(/Row:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
|
|
43
|
+
if (!rowMatch) return columns;
|
|
44
|
+
const rowContent = rowMatch[1];
|
|
45
|
+
const includePrimaryKey = options.syncPrimaryKey ?? options.includeId ?? false;
|
|
46
|
+
const columnRegex = /(\w+)\??:\s*([^,\n]+)/g;
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = columnRegex.exec(rowContent)) !== null) {
|
|
49
|
+
const [, columnName, columnType] = match;
|
|
50
|
+
const shouldSkip = options.skipColumns.has(columnName) && !(includePrimaryKey && columnName === "id");
|
|
51
|
+
if (!shouldSkip) {
|
|
52
|
+
columns.set(columnName, columnType.trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return columns;
|
|
56
|
+
}
|
|
57
|
+
function extractTableDef(content, tableName, schema) {
|
|
58
|
+
const schemaRegex = new RegExp(
|
|
59
|
+
`${schema}:\\s*\\{[\\s\\S]*?Tables:\\s*\\{`,
|
|
60
|
+
"g"
|
|
61
|
+
);
|
|
62
|
+
const schemaMatch = schemaRegex.exec(content);
|
|
63
|
+
if (!schemaMatch) return null;
|
|
64
|
+
const startIndex = schemaMatch.index;
|
|
65
|
+
const tableRegex = new RegExp(
|
|
66
|
+
`(?<![A-Za-z])${tableName}:\\s*\\{[\\s\\S]*?Row:\\s*\\{[\\s\\S]*?\\}[\\s\\S]*?Relationships:\\s*\\[[^\\]]*\\]\\s*\\}`,
|
|
67
|
+
"g"
|
|
68
|
+
);
|
|
69
|
+
const searchContent = content.slice(startIndex);
|
|
70
|
+
const tableMatch = tableRegex.exec(searchContent);
|
|
71
|
+
return tableMatch ? tableMatch[0] : null;
|
|
72
|
+
}
|
|
73
|
+
function parseTypesFile(content, tables, skipColumns) {
|
|
74
|
+
const parsedTables = [];
|
|
75
|
+
for (const tableConfig of tables) {
|
|
76
|
+
const { name, schema = "public", syncPrimaryKey, includeId } = tableConfig;
|
|
77
|
+
const tableDef = extractTableDef(content, name, schema);
|
|
78
|
+
if (!tableDef) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const columns = parseRowType(tableDef, {
|
|
82
|
+
skipColumns,
|
|
83
|
+
syncPrimaryKey,
|
|
84
|
+
includeId
|
|
85
|
+
});
|
|
86
|
+
if (columns.size > 0) {
|
|
87
|
+
parsedTables.push({
|
|
88
|
+
name,
|
|
89
|
+
schema,
|
|
90
|
+
columns,
|
|
91
|
+
config: tableConfig
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return parsedTables;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/generator/templates.ts
|
|
99
|
+
function toSnakeCase(str) {
|
|
100
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function generateIndexDefinitions(tableName, columns, indexPatterns, additionalColumns = []) {
|
|
103
|
+
const indexes = [];
|
|
104
|
+
const snakeTableName = toSnakeCase(tableName);
|
|
105
|
+
const columnsToIndex = /* @__PURE__ */ new Set();
|
|
106
|
+
for (const column of columns) {
|
|
107
|
+
if (column === "id") continue;
|
|
108
|
+
for (const pattern of indexPatterns) {
|
|
109
|
+
const regex = new RegExp(pattern);
|
|
110
|
+
if (regex.test(column)) {
|
|
111
|
+
columnsToIndex.add(column);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const column of additionalColumns) {
|
|
117
|
+
if (columns.includes(column) && column !== "id") {
|
|
118
|
+
columnsToIndex.add(column);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const column of columnsToIndex) {
|
|
122
|
+
const snakeColumn = toSnakeCase(column);
|
|
123
|
+
indexes.push({
|
|
124
|
+
name: `idx_${snakeTableName}_${snakeColumn}`,
|
|
125
|
+
columns: [column]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
indexes.sort((a, b) => a.name.localeCompare(b.name));
|
|
129
|
+
return indexes;
|
|
130
|
+
}
|
|
131
|
+
function generateHeader(typesPath) {
|
|
132
|
+
return `/**
|
|
133
|
+
* PowerSync Schema Definition
|
|
134
|
+
*
|
|
135
|
+
* AUTO-GENERATED from ${typesPath}
|
|
136
|
+
* Run: npx @pol-studios/powersync generate-schema
|
|
137
|
+
*
|
|
138
|
+
* DO NOT EDIT MANUALLY - changes will be overwritten
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
import { column, Schema, Table } from "@powersync/react-native";
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
function formatIndexes(indexes) {
|
|
145
|
+
if (indexes.length === 0) return "";
|
|
146
|
+
const indexLines = indexes.map((idx) => {
|
|
147
|
+
const columnsStr = idx.columns.map((c) => `'${c}'`).join(", ");
|
|
148
|
+
return ` { name: '${idx.name}', columns: [${columnsStr}] },`;
|
|
149
|
+
});
|
|
150
|
+
return `indexes: [
|
|
151
|
+
${indexLines.join("\n")}
|
|
152
|
+
]`;
|
|
153
|
+
}
|
|
154
|
+
function generateTableDefinition(table, columnDefs, indexes = []) {
|
|
155
|
+
if (columnDefs.length === 0) {
|
|
156
|
+
return `// ${table.name} - no syncable columns found`;
|
|
157
|
+
}
|
|
158
|
+
const optionsParts = [];
|
|
159
|
+
if (table.config.trackMetadata) {
|
|
160
|
+
optionsParts.push("trackMetadata: true");
|
|
161
|
+
}
|
|
162
|
+
if (indexes.length > 0) {
|
|
163
|
+
optionsParts.push(formatIndexes(indexes));
|
|
164
|
+
}
|
|
165
|
+
const optionsStr = optionsParts.length > 0 ? `, {
|
|
166
|
+
${optionsParts.join(",\n ")}
|
|
167
|
+
}` : "";
|
|
168
|
+
return `const ${table.name} = new Table({
|
|
169
|
+
${columnDefs.join("\n")}
|
|
170
|
+
}${optionsStr});`;
|
|
171
|
+
}
|
|
172
|
+
function generateSchemaExport(tableNames) {
|
|
173
|
+
return `// ============================================================================
|
|
174
|
+
// SCHEMA EXPORT
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
// NOTE: photo_attachments is NOT included here.
|
|
178
|
+
// The AttachmentQueue from @powersync/attachments creates and manages
|
|
179
|
+
// its own internal SQLite table (not a view) during queue.init().
|
|
180
|
+
// This allows INSERT/UPDATE operations to work correctly.
|
|
181
|
+
|
|
182
|
+
export const AppSchema = new Schema({
|
|
183
|
+
${tableNames.map((name) => ` ${name},`).join("\n")}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export type Database = (typeof AppSchema)["types"];`;
|
|
187
|
+
}
|
|
188
|
+
function generateSchemaMapping(tables, schemas) {
|
|
189
|
+
const schemaGroups = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const schema of schemas) {
|
|
191
|
+
if (schema !== "public") {
|
|
192
|
+
schemaGroups.set(schema, []);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const table of tables) {
|
|
196
|
+
if (table.schema !== "public" && schemaGroups.has(table.schema)) {
|
|
197
|
+
schemaGroups.get(table.schema).push(table.name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const sections = [
|
|
201
|
+
`// ============================================================================
|
|
202
|
+
// SCHEMA MAPPING FOR CONNECTOR
|
|
203
|
+
// ============================================================================`
|
|
204
|
+
];
|
|
205
|
+
for (const [schema, tableNames] of schemaGroups) {
|
|
206
|
+
if (tableNames.length > 0) {
|
|
207
|
+
const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;
|
|
208
|
+
sections.push(`
|
|
209
|
+
// Tables in the '${schema}' schema (need .schema('${schema}') in Supabase queries)
|
|
210
|
+
export const ${constName} = new Set([
|
|
211
|
+
${tableNames.map((name) => ` "${name}",`).join("\n")}
|
|
212
|
+
]);`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const schemaChecks = Array.from(schemaGroups.entries()).filter(([, names]) => names.length > 0).map(([schema]) => {
|
|
216
|
+
const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;
|
|
217
|
+
return ` if (${constName}.has(tableName)) return "${schema}";`;
|
|
218
|
+
});
|
|
219
|
+
if (schemaChecks.length > 0) {
|
|
220
|
+
sections.push(`
|
|
221
|
+
/**
|
|
222
|
+
* Get the Supabase schema for a table
|
|
223
|
+
*/
|
|
224
|
+
export function getTableSchema(tableName: string): ${schemas.map((s) => `"${s}"`).join(" | ")} {
|
|
225
|
+
${schemaChecks.join("\n")}
|
|
226
|
+
return "public";
|
|
227
|
+
}`);
|
|
228
|
+
} else {
|
|
229
|
+
sections.push(`
|
|
230
|
+
/**
|
|
231
|
+
* Get the Supabase schema for a table
|
|
232
|
+
*/
|
|
233
|
+
export function getTableSchema(tableName: string): "public" {
|
|
234
|
+
return "public";
|
|
235
|
+
}`);
|
|
236
|
+
}
|
|
237
|
+
return sections.join("\n");
|
|
238
|
+
}
|
|
239
|
+
function generateFKUtility() {
|
|
240
|
+
return `
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// FOREIGN KEY UTILITIES
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a column name represents a foreign key reference
|
|
247
|
+
* Convention: columns ending in 'Id' are foreign keys (e.g., projectId -> Project table)
|
|
248
|
+
*/
|
|
249
|
+
export function isForeignKeyColumn(columnName: string): boolean {
|
|
250
|
+
return columnName.endsWith('Id') && columnName !== 'id';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the referenced table name from a foreign key column
|
|
255
|
+
* e.g., 'projectId' -> 'Project', 'equipmentFixtureUnitId' -> 'EquipmentFixtureUnit'
|
|
256
|
+
*/
|
|
257
|
+
export function getForeignKeyTable(columnName: string): string | null {
|
|
258
|
+
if (!isForeignKeyColumn(columnName)) return null;
|
|
259
|
+
// Remove 'Id' suffix and capitalize first letter
|
|
260
|
+
const baseName = columnName.slice(0, -2);
|
|
261
|
+
return baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
262
|
+
}`;
|
|
263
|
+
}
|
|
264
|
+
function generateOutputFile(tables, tableDefs, schemas, typesPath) {
|
|
265
|
+
const tableNames = tables.map((t) => t.name);
|
|
266
|
+
return `${generateHeader(typesPath)}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// TABLE DEFINITIONS
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
${tableDefs.join("\n\n")}
|
|
273
|
+
|
|
274
|
+
${generateSchemaExport(tableNames)}
|
|
275
|
+
|
|
276
|
+
${generateSchemaMapping(tables, ["public", ...schemas.filter((s) => s !== "public")])}
|
|
277
|
+
${generateFKUtility()}
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/generator/fk-dependencies.ts
|
|
282
|
+
function fkColumnToTableName(columnName) {
|
|
283
|
+
if (!columnName.endsWith("Id") || columnName === "id") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const baseName = columnName.slice(0, -2);
|
|
287
|
+
return baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
288
|
+
}
|
|
289
|
+
function detectFKDependencies(tables) {
|
|
290
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
291
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
292
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
293
|
+
const fkRelationships = [];
|
|
294
|
+
for (const table of tables) {
|
|
295
|
+
dependencies.set(table.name, []);
|
|
296
|
+
dependents.set(table.name, []);
|
|
297
|
+
}
|
|
298
|
+
for (const table of tables) {
|
|
299
|
+
for (const [columnName] of table.columns) {
|
|
300
|
+
const referencedTable = fkColumnToTableName(columnName);
|
|
301
|
+
if (referencedTable && tableNames.has(referencedTable)) {
|
|
302
|
+
const tableDeps = dependencies.get(table.name) || [];
|
|
303
|
+
if (!tableDeps.includes(referencedTable)) {
|
|
304
|
+
tableDeps.push(referencedTable);
|
|
305
|
+
dependencies.set(table.name, tableDeps);
|
|
306
|
+
}
|
|
307
|
+
const refDeps = dependents.get(referencedTable) || [];
|
|
308
|
+
if (!refDeps.includes(table.name)) {
|
|
309
|
+
refDeps.push(table.name);
|
|
310
|
+
dependents.set(referencedTable, refDeps);
|
|
311
|
+
}
|
|
312
|
+
fkRelationships.push({
|
|
313
|
+
table: table.name,
|
|
314
|
+
column: columnName,
|
|
315
|
+
referencedTable
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const uploadOrder = getUploadOrder({ dependencies });
|
|
321
|
+
return {
|
|
322
|
+
dependencies,
|
|
323
|
+
dependents,
|
|
324
|
+
uploadOrder,
|
|
325
|
+
fkRelationships
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function getUploadOrder(graph) {
|
|
329
|
+
const result = [];
|
|
330
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
331
|
+
const adjList = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const [table, deps] of graph.dependencies) {
|
|
333
|
+
inDegree.set(table, deps.length);
|
|
334
|
+
adjList.set(table, []);
|
|
335
|
+
}
|
|
336
|
+
for (const [table, deps] of graph.dependencies) {
|
|
337
|
+
for (const dep of deps) {
|
|
338
|
+
const list = adjList.get(dep) || [];
|
|
339
|
+
list.push(table);
|
|
340
|
+
adjList.set(dep, list);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const queue = [];
|
|
344
|
+
for (const [table, degree] of inDegree) {
|
|
345
|
+
if (degree === 0) {
|
|
346
|
+
queue.push(table);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
queue.sort();
|
|
350
|
+
while (queue.length > 0) {
|
|
351
|
+
const table = queue.shift();
|
|
352
|
+
result.push(table);
|
|
353
|
+
const dependentTables = adjList.get(table) || [];
|
|
354
|
+
for (const dependent of dependentTables) {
|
|
355
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
356
|
+
inDegree.set(dependent, newDegree);
|
|
357
|
+
if (newDegree === 0) {
|
|
358
|
+
queue.push(dependent);
|
|
359
|
+
queue.sort();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (result.length < graph.dependencies.size) {
|
|
364
|
+
const remaining = [...graph.dependencies.keys()].filter((t) => !result.includes(t)).sort();
|
|
365
|
+
result.push(...remaining);
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/generator/generator.ts
|
|
371
|
+
function mapTypeToPowerSync(tsType, columnName, decimalPatterns) {
|
|
372
|
+
const cleanType = tsType.trim().replace(/\s*\|\s*null/g, "");
|
|
373
|
+
if (cleanType.includes("Json") || cleanType.includes("unknown") || cleanType.includes("{")) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (cleanType.includes("[]")) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (cleanType === "boolean") {
|
|
380
|
+
return { type: "column.integer", isBoolean: true };
|
|
381
|
+
}
|
|
382
|
+
if (cleanType === "number") {
|
|
383
|
+
if (decimalPatterns.some(
|
|
384
|
+
(pattern) => columnName.toLowerCase().includes(pattern.toLowerCase())
|
|
385
|
+
)) {
|
|
386
|
+
return { type: "column.real" };
|
|
387
|
+
}
|
|
388
|
+
return { type: "column.integer" };
|
|
389
|
+
}
|
|
390
|
+
if (cleanType === "string") {
|
|
391
|
+
return { type: "column.text" };
|
|
392
|
+
}
|
|
393
|
+
if (cleanType.includes("Database[") && cleanType.includes("Enums")) {
|
|
394
|
+
return { type: "column.text", isEnum: true };
|
|
395
|
+
}
|
|
396
|
+
return { type: "column.text" };
|
|
397
|
+
}
|
|
398
|
+
function generateColumnDefs(table, decimalPatterns) {
|
|
399
|
+
const columnDefs = [];
|
|
400
|
+
for (const [columnName, tsType] of table.columns) {
|
|
401
|
+
const mapping = mapTypeToPowerSync(tsType, columnName, decimalPatterns);
|
|
402
|
+
if (mapping) {
|
|
403
|
+
let comment = "";
|
|
404
|
+
if (mapping.isBoolean) {
|
|
405
|
+
comment = " // boolean stored as 0/1";
|
|
406
|
+
} else if (mapping.isEnum) {
|
|
407
|
+
comment = " // enum stored as text";
|
|
408
|
+
}
|
|
409
|
+
columnDefs.push(` ${columnName}: ${mapping.type},${comment}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return columnDefs;
|
|
413
|
+
}
|
|
414
|
+
async function generateSchema(config, options) {
|
|
415
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
416
|
+
const verbose = options?.verbose ?? false;
|
|
417
|
+
const dryRun = options?.dryRun ?? false;
|
|
418
|
+
const result = {
|
|
419
|
+
success: false,
|
|
420
|
+
tablesGenerated: 0,
|
|
421
|
+
outputPath: "",
|
|
422
|
+
errors: [],
|
|
423
|
+
warnings: []
|
|
424
|
+
};
|
|
425
|
+
const typesPath = path.isAbsolute(config.typesPath) ? config.typesPath : path.resolve(cwd, config.typesPath);
|
|
426
|
+
const outputPath = path.isAbsolute(config.outputPath) ? config.outputPath : path.resolve(cwd, config.outputPath);
|
|
427
|
+
result.outputPath = outputPath;
|
|
428
|
+
if (!fs.existsSync(typesPath)) {
|
|
429
|
+
result.errors.push(`Types file not found: ${typesPath}`);
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
if (verbose) {
|
|
433
|
+
console.log(`Reading types from: ${typesPath}`);
|
|
434
|
+
}
|
|
435
|
+
const typesContent = fs.readFileSync(typesPath, "utf-8");
|
|
436
|
+
const skipColumns = /* @__PURE__ */ new Set([
|
|
437
|
+
...DEFAULT_SKIP_COLUMNS,
|
|
438
|
+
...config.skipColumns ?? []
|
|
439
|
+
]);
|
|
440
|
+
const decimalPatterns = [
|
|
441
|
+
...DEFAULT_DECIMAL_PATTERNS,
|
|
442
|
+
...config.decimalPatterns ?? []
|
|
443
|
+
];
|
|
444
|
+
const parsedTables = parseTypesFile(
|
|
445
|
+
typesContent,
|
|
446
|
+
config.tables,
|
|
447
|
+
skipColumns
|
|
448
|
+
);
|
|
449
|
+
for (const tableConfig of config.tables) {
|
|
450
|
+
const found = parsedTables.some((t) => t.name === tableConfig.name);
|
|
451
|
+
if (!found) {
|
|
452
|
+
result.warnings.push(
|
|
453
|
+
`Table '${tableConfig.name}' not found in schema '${tableConfig.schema ?? "public"}'`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (parsedTables.length === 0) {
|
|
458
|
+
result.errors.push("No tables were parsed successfully");
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
const autoIndexes = config.autoIndexes ?? true;
|
|
462
|
+
const indexPatterns = autoIndexes ? [...DEFAULT_INDEX_PATTERNS, ...config.indexPatterns ?? []] : config.indexPatterns ?? [];
|
|
463
|
+
const indexColumns = config.indexColumns ?? [];
|
|
464
|
+
const dependencyGraph = detectFKDependencies(parsedTables);
|
|
465
|
+
result.dependencyGraph = dependencyGraph;
|
|
466
|
+
if (verbose && dependencyGraph.fkRelationships.length > 0) {
|
|
467
|
+
console.log(`
|
|
468
|
+
FK Dependencies detected: ${dependencyGraph.fkRelationships.length}`);
|
|
469
|
+
for (const fk of dependencyGraph.fkRelationships) {
|
|
470
|
+
console.log(` ${fk.table}.${fk.column} -> ${fk.referencedTable}`);
|
|
471
|
+
}
|
|
472
|
+
console.log(`
|
|
473
|
+
Recommended upload order:`);
|
|
474
|
+
dependencyGraph.uploadOrder.forEach((table, i) => {
|
|
475
|
+
console.log(` ${i + 1}. ${table}`);
|
|
476
|
+
});
|
|
477
|
+
console.log("");
|
|
478
|
+
}
|
|
479
|
+
const tableDefs = [];
|
|
480
|
+
let totalIndexes = 0;
|
|
481
|
+
for (const table of parsedTables) {
|
|
482
|
+
if (verbose) {
|
|
483
|
+
const syncPK = table.config.syncPrimaryKey || table.config.includeId;
|
|
484
|
+
console.log(
|
|
485
|
+
`Processing ${table.schema}.${table.name} (${table.columns.size} columns)${table.config.trackMetadata ? " [trackMetadata]" : ""}${syncPK ? " [syncPrimaryKey]" : ""}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const columnDefs = generateColumnDefs(table, decimalPatterns);
|
|
489
|
+
if (columnDefs.length === 0) {
|
|
490
|
+
result.warnings.push(`Table '${table.name}' has no syncable columns`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const columnNames = [...table.columns.keys()];
|
|
494
|
+
const indexes = indexPatterns.length > 0 || indexColumns.length > 0 ? generateIndexDefinitions(table.name, columnNames, indexPatterns, indexColumns) : [];
|
|
495
|
+
totalIndexes += indexes.length;
|
|
496
|
+
if (verbose && indexes.length > 0) {
|
|
497
|
+
console.log(` Indexes: ${indexes.map((i) => i.name).join(", ")}`);
|
|
498
|
+
}
|
|
499
|
+
tableDefs.push(generateTableDefinition(table, columnDefs, indexes));
|
|
500
|
+
}
|
|
501
|
+
result.indexesGenerated = totalIndexes;
|
|
502
|
+
const schemas = [...new Set(config.tables.map((t) => t.schema ?? "public"))];
|
|
503
|
+
const relativePath = path.relative(cwd, typesPath);
|
|
504
|
+
const output = generateOutputFile(
|
|
505
|
+
parsedTables.filter(
|
|
506
|
+
(t) => tableDefs.some((def) => def.includes(`const ${t.name} =`))
|
|
507
|
+
),
|
|
508
|
+
tableDefs,
|
|
509
|
+
schemas,
|
|
510
|
+
relativePath
|
|
511
|
+
);
|
|
512
|
+
if (dryRun) {
|
|
513
|
+
result.success = true;
|
|
514
|
+
result.tablesGenerated = tableDefs.length;
|
|
515
|
+
result.output = output;
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
const outputDir = path.dirname(outputPath);
|
|
519
|
+
if (!fs.existsSync(outputDir)) {
|
|
520
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
521
|
+
}
|
|
522
|
+
fs.writeFileSync(outputPath, output);
|
|
523
|
+
result.success = true;
|
|
524
|
+
result.tablesGenerated = tableDefs.length;
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/generator/cli.ts
|
|
529
|
+
var program = new Command();
|
|
530
|
+
var VERSION = "1.0.0";
|
|
531
|
+
program.name("@pol-studios/powersync").description("PowerSync utilities for offline-first applications").version(VERSION);
|
|
532
|
+
program.command("generate-schema").description("Generate PowerSync schema from database.types.ts").option(
|
|
533
|
+
"-c, --config <path>",
|
|
534
|
+
"Path to config file",
|
|
535
|
+
"powersync.config.ts"
|
|
536
|
+
).option("-v, --verbose", "Enable verbose output", false).option("-d, --dry-run", "Preview output without writing files", false).option("-w, --watch", "Watch for changes and regenerate", false).action(async (options) => {
|
|
537
|
+
const cwd = process.cwd();
|
|
538
|
+
console.log(pc.bold(pc.cyan("\n PowerSync Schema Generator\n")));
|
|
539
|
+
const configPath = path2.isAbsolute(options.config) ? options.config : path2.resolve(cwd, options.config);
|
|
540
|
+
if (!fs2.existsSync(configPath)) {
|
|
541
|
+
console.error(pc.red(` Error: Config file not found: ${configPath}`));
|
|
542
|
+
console.log(pc.dim(`
|
|
543
|
+
Create a powersync.config.ts file in your project root:
|
|
544
|
+
|
|
545
|
+
${pc.cyan(`import { defineConfig } from '@pol-studios/powersync/generator';
|
|
546
|
+
|
|
547
|
+
export default defineConfig({
|
|
548
|
+
typesPath: './database.types.ts',
|
|
549
|
+
outputPath: './src/data/powersync-schema.ts',
|
|
550
|
+
tables: [
|
|
551
|
+
{ name: 'User', schema: 'public' },
|
|
552
|
+
{ name: 'Post', schema: 'public', trackMetadata: true },
|
|
553
|
+
],
|
|
554
|
+
});`)}
|
|
555
|
+
`));
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
let config;
|
|
559
|
+
try {
|
|
560
|
+
console.log(pc.dim(` Loading config from: ${configPath}`));
|
|
561
|
+
const configModule = await import(configPath);
|
|
562
|
+
config = configModule.default;
|
|
563
|
+
if (!config || !config.tables || !config.typesPath || !config.outputPath) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
"Invalid config: must export { typesPath, outputPath, tables }"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.error(pc.red(` Error loading config: ${error}`));
|
|
570
|
+
console.log(pc.dim(`
|
|
571
|
+
Make sure your config file:
|
|
572
|
+
1. Uses 'export default defineConfig({...})'
|
|
573
|
+
2. Contains typesPath, outputPath, and tables properties
|
|
574
|
+
3. Can be imported (run with tsx or ts-node if using TypeScript)
|
|
575
|
+
`));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const runGeneration = async () => {
|
|
579
|
+
console.log(pc.dim(` Types file: ${config.typesPath}`));
|
|
580
|
+
console.log(pc.dim(` Output file: ${config.outputPath}`));
|
|
581
|
+
console.log(pc.dim(` Tables: ${config.tables.length}`));
|
|
582
|
+
if (options.dryRun) {
|
|
583
|
+
console.log(pc.cyan(" Mode: dry-run (no files will be written)"));
|
|
584
|
+
}
|
|
585
|
+
console.log("");
|
|
586
|
+
const result = await generateSchema(config, {
|
|
587
|
+
cwd,
|
|
588
|
+
verbose: options.verbose,
|
|
589
|
+
dryRun: options.dryRun
|
|
590
|
+
});
|
|
591
|
+
if (result.warnings.length > 0) {
|
|
592
|
+
console.log(pc.yellow(" Warnings:"));
|
|
593
|
+
for (const warning of result.warnings) {
|
|
594
|
+
console.log(pc.yellow(` - ${warning}`));
|
|
595
|
+
}
|
|
596
|
+
console.log("");
|
|
597
|
+
}
|
|
598
|
+
if (result.errors.length > 0) {
|
|
599
|
+
console.log(pc.red(" Errors:"));
|
|
600
|
+
for (const error of result.errors) {
|
|
601
|
+
console.log(pc.red(` - ${error}`));
|
|
602
|
+
}
|
|
603
|
+
console.log("");
|
|
604
|
+
}
|
|
605
|
+
if (result.success) {
|
|
606
|
+
if (options.dryRun) {
|
|
607
|
+
console.log(
|
|
608
|
+
pc.cyan(` ${pc.bold("Dry run:")} Would generate ${result.tablesGenerated} tables`)
|
|
609
|
+
);
|
|
610
|
+
if (result.indexesGenerated !== void 0 && result.indexesGenerated > 0) {
|
|
611
|
+
console.log(pc.cyan(` Indexes: ${result.indexesGenerated}`));
|
|
612
|
+
}
|
|
613
|
+
if (options.verbose && result.output) {
|
|
614
|
+
console.log(pc.dim("\n Generated output:\n"));
|
|
615
|
+
console.log(pc.dim(result.output.split("\n").map((l) => " " + l).join("\n")));
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
console.log(
|
|
619
|
+
pc.green(` ${pc.bold("Success!")} Generated ${result.tablesGenerated} tables`)
|
|
620
|
+
);
|
|
621
|
+
if (result.indexesGenerated !== void 0 && result.indexesGenerated > 0) {
|
|
622
|
+
console.log(pc.green(` Indexes: ${result.indexesGenerated}`));
|
|
623
|
+
}
|
|
624
|
+
if (result.dependencyGraph && result.dependencyGraph.fkRelationships.length > 0) {
|
|
625
|
+
console.log(pc.green(` FK dependencies: ${result.dependencyGraph.fkRelationships.length}`));
|
|
626
|
+
}
|
|
627
|
+
console.log(pc.dim(` Output: ${result.outputPath}`));
|
|
628
|
+
}
|
|
629
|
+
console.log("");
|
|
630
|
+
} else {
|
|
631
|
+
console.error(pc.red(" Schema generation failed"));
|
|
632
|
+
if (!options.watch) {
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return result;
|
|
637
|
+
};
|
|
638
|
+
await runGeneration();
|
|
639
|
+
if (options.watch) {
|
|
640
|
+
const typesPath = path2.isAbsolute(config.typesPath) ? config.typesPath : path2.resolve(cwd, config.typesPath);
|
|
641
|
+
console.log(pc.cyan(` Watching for changes: ${typesPath}`));
|
|
642
|
+
console.log(pc.dim(" Press Ctrl+C to stop\n"));
|
|
643
|
+
fs2.watchFile(typesPath, { interval: 1e3 }, async (curr, prev) => {
|
|
644
|
+
if (curr.mtime !== prev.mtime) {
|
|
645
|
+
console.log(pc.cyan("\n File changed, regenerating...\n"));
|
|
646
|
+
await runGeneration();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
process.on("SIGINT", () => {
|
|
650
|
+
fs2.unwatchFile(typesPath);
|
|
651
|
+
console.log(pc.dim("\n Stopped watching"));
|
|
652
|
+
process.exit(0);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
program.command("init").description("Create a powersync.config.ts template").option("-f, --force", "Overwrite existing config file", false).action((options) => {
|
|
657
|
+
const cwd = process.cwd();
|
|
658
|
+
const configPath = path2.resolve(cwd, "powersync.config.ts");
|
|
659
|
+
if (fs2.existsSync(configPath) && !options.force) {
|
|
660
|
+
console.error(
|
|
661
|
+
pc.red(` Config file already exists: ${configPath}`)
|
|
662
|
+
);
|
|
663
|
+
console.log(pc.dim(" Use --force to overwrite"));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
const template = `import { defineConfig } from '@pol-studios/powersync/generator';
|
|
667
|
+
|
|
668
|
+
export default defineConfig({
|
|
669
|
+
// Path to Supabase-generated types
|
|
670
|
+
typesPath: './database.types.ts',
|
|
671
|
+
|
|
672
|
+
// Output path for generated schema
|
|
673
|
+
outputPath: './src/data/powersync-schema.ts',
|
|
674
|
+
|
|
675
|
+
// Tables to sync
|
|
676
|
+
tables: [
|
|
677
|
+
// Public schema tables (default)
|
|
678
|
+
{ name: 'User' },
|
|
679
|
+
{ name: 'Post', trackMetadata: true },
|
|
680
|
+
|
|
681
|
+
// Tables in other schemas
|
|
682
|
+
// { name: 'Profile', schema: 'core' },
|
|
683
|
+
|
|
684
|
+
// For tables with integer PKs referenced by FKs in other tables,
|
|
685
|
+
// use syncPrimaryKey: true to include the id column
|
|
686
|
+
// { name: 'Group', schema: 'core', syncPrimaryKey: true },
|
|
687
|
+
|
|
688
|
+
// Access control tables (required for offline auth)
|
|
689
|
+
// { name: 'UserAccess', schema: 'core' },
|
|
690
|
+
// { name: 'UserGroup', schema: 'core' },
|
|
691
|
+
// { name: 'GroupAccessKey', schema: 'core', syncPrimaryKey: true },
|
|
692
|
+
// { name: 'Group', schema: 'core', syncPrimaryKey: true },
|
|
693
|
+
],
|
|
694
|
+
|
|
695
|
+
// Optional: columns to always skip
|
|
696
|
+
// skipColumns: ['searchVector', 'tsv'],
|
|
697
|
+
|
|
698
|
+
// Optional: column name patterns for decimal values (use column.real)
|
|
699
|
+
// decimalPatterns: ['price', 'amount', 'rate'],
|
|
700
|
+
|
|
701
|
+
// Auto-generate indexes for FK columns and common patterns (default: true)
|
|
702
|
+
// autoIndexes: true,
|
|
703
|
+
|
|
704
|
+
// Additional columns to index (exact names)
|
|
705
|
+
// indexColumns: ['name', 'email'],
|
|
706
|
+
|
|
707
|
+
// Additional index patterns (regex, in addition to defaults: Id$, createdAt, updatedAt, status, type)
|
|
708
|
+
// indexPatterns: ['^sortOrder$'],
|
|
709
|
+
});
|
|
710
|
+
`;
|
|
711
|
+
fs2.writeFileSync(configPath, template);
|
|
712
|
+
console.log(pc.green(` Created config file: ${configPath}`));
|
|
713
|
+
console.log(pc.dim(`
|
|
714
|
+
Next steps:
|
|
715
|
+
1. Edit powersync.config.ts with your tables
|
|
716
|
+
2. Run: npx @pol-studios/powersync generate-schema
|
|
717
|
+
`));
|
|
718
|
+
});
|
|
719
|
+
program.parse();
|