@pol-studios/powersync 1.0.11 → 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 +195 -3
- package/dist/generator/index.d.ts +91 -2
- package/dist/generator/index.js +223 -3
- package/dist/generator/index.js.map +1 -1
- package/package.json +1 -1
package/dist/generator/cli.js
CHANGED
|
@@ -27,6 +27,14 @@ var DEFAULT_DECIMAL_PATTERNS = [
|
|
|
27
27
|
"cost",
|
|
28
28
|
"total"
|
|
29
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
|
+
];
|
|
30
38
|
|
|
31
39
|
// src/generator/parser.ts
|
|
32
40
|
function parseRowType(tableContent, options) {
|
|
@@ -88,6 +96,38 @@ function parseTypesFile(content, tables, skipColumns) {
|
|
|
88
96
|
}
|
|
89
97
|
|
|
90
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
|
+
}
|
|
91
131
|
function generateHeader(typesPath) {
|
|
92
132
|
return `/**
|
|
93
133
|
* PowerSync Schema Definition
|
|
@@ -101,11 +141,30 @@ function generateHeader(typesPath) {
|
|
|
101
141
|
import { column, Schema, Table } from "@powersync/react-native";
|
|
102
142
|
`;
|
|
103
143
|
}
|
|
104
|
-
function
|
|
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 = []) {
|
|
105
155
|
if (columnDefs.length === 0) {
|
|
106
156
|
return `// ${table.name} - no syncable columns found`;
|
|
107
157
|
}
|
|
108
|
-
const
|
|
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
|
+
}` : "";
|
|
109
168
|
return `const ${table.name} = new Table({
|
|
110
169
|
${columnDefs.join("\n")}
|
|
111
170
|
}${optionsStr});`;
|
|
@@ -219,6 +278,95 @@ ${generateFKUtility()}
|
|
|
219
278
|
`;
|
|
220
279
|
}
|
|
221
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
|
+
|
|
222
370
|
// src/generator/generator.ts
|
|
223
371
|
function mapTypeToPowerSync(tsType, columnName, decimalPatterns) {
|
|
224
372
|
const cleanType = tsType.trim().replace(/\s*\|\s*null/g, "");
|
|
@@ -310,7 +458,26 @@ async function generateSchema(config, options) {
|
|
|
310
458
|
result.errors.push("No tables were parsed successfully");
|
|
311
459
|
return result;
|
|
312
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
|
+
}
|
|
313
479
|
const tableDefs = [];
|
|
480
|
+
let totalIndexes = 0;
|
|
314
481
|
for (const table of parsedTables) {
|
|
315
482
|
if (verbose) {
|
|
316
483
|
const syncPK = table.config.syncPrimaryKey || table.config.includeId;
|
|
@@ -323,8 +490,15 @@ async function generateSchema(config, options) {
|
|
|
323
490
|
result.warnings.push(`Table '${table.name}' has no syncable columns`);
|
|
324
491
|
continue;
|
|
325
492
|
}
|
|
326
|
-
|
|
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));
|
|
327
500
|
}
|
|
501
|
+
result.indexesGenerated = totalIndexes;
|
|
328
502
|
const schemas = [...new Set(config.tables.map((t) => t.schema ?? "public"))];
|
|
329
503
|
const relativePath = path.relative(cwd, typesPath);
|
|
330
504
|
const output = generateOutputFile(
|
|
@@ -433,6 +607,9 @@ program.command("generate-schema").description("Generate PowerSync schema from d
|
|
|
433
607
|
console.log(
|
|
434
608
|
pc.cyan(` ${pc.bold("Dry run:")} Would generate ${result.tablesGenerated} tables`)
|
|
435
609
|
);
|
|
610
|
+
if (result.indexesGenerated !== void 0 && result.indexesGenerated > 0) {
|
|
611
|
+
console.log(pc.cyan(` Indexes: ${result.indexesGenerated}`));
|
|
612
|
+
}
|
|
436
613
|
if (options.verbose && result.output) {
|
|
437
614
|
console.log(pc.dim("\n Generated output:\n"));
|
|
438
615
|
console.log(pc.dim(result.output.split("\n").map((l) => " " + l).join("\n")));
|
|
@@ -441,6 +618,12 @@ program.command("generate-schema").description("Generate PowerSync schema from d
|
|
|
441
618
|
console.log(
|
|
442
619
|
pc.green(` ${pc.bold("Success!")} Generated ${result.tablesGenerated} tables`)
|
|
443
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
|
+
}
|
|
444
627
|
console.log(pc.dim(` Output: ${result.outputPath}`));
|
|
445
628
|
}
|
|
446
629
|
console.log("");
|
|
@@ -514,6 +697,15 @@ export default defineConfig({
|
|
|
514
697
|
|
|
515
698
|
// Optional: column name patterns for decimal values (use column.real)
|
|
516
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$'],
|
|
517
709
|
});
|
|
518
710
|
`;
|
|
519
711
|
fs2.writeFileSync(configPath, template);
|
|
@@ -36,6 +36,12 @@ interface GeneratorConfig {
|
|
|
36
36
|
decimalPatterns?: string[];
|
|
37
37
|
/** Additional schemas to track (besides 'public' which is the default) */
|
|
38
38
|
schemas?: string[];
|
|
39
|
+
/** Generate indexes for FK and common columns (default: true) */
|
|
40
|
+
autoIndexes?: boolean;
|
|
41
|
+
/** Additional columns to index (exact column names) */
|
|
42
|
+
indexColumns?: string[];
|
|
43
|
+
/** Column patterns that should be indexed (regex patterns) */
|
|
44
|
+
indexPatterns?: string[];
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Define a PowerSync generator configuration with type safety
|
|
@@ -49,6 +55,11 @@ declare const DEFAULT_SKIP_COLUMNS: string[];
|
|
|
49
55
|
* Default column name patterns that indicate decimal values
|
|
50
56
|
*/
|
|
51
57
|
declare const DEFAULT_DECIMAL_PATTERNS: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Default column patterns that should be indexed
|
|
60
|
+
* These are regex patterns matched against column names
|
|
61
|
+
*/
|
|
62
|
+
declare const DEFAULT_INDEX_PATTERNS: string[];
|
|
52
63
|
|
|
53
64
|
/**
|
|
54
65
|
* Parser for Supabase database.types.ts files
|
|
@@ -99,6 +110,66 @@ declare function getAvailableSchemas(content: string): string[];
|
|
|
99
110
|
*/
|
|
100
111
|
declare function getTablesInSchema(content: string, schema: string): string[];
|
|
101
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Foreign Key Dependency Detection
|
|
115
|
+
*
|
|
116
|
+
* Analyzes parsed tables to detect FK relationships and determine
|
|
117
|
+
* optimal upload order for offline-first sync.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
interface FKDependency {
|
|
121
|
+
/** Table containing the FK column */
|
|
122
|
+
table: string;
|
|
123
|
+
/** FK column name (e.g., 'projectId') */
|
|
124
|
+
column: string;
|
|
125
|
+
/** Referenced table name (e.g., 'Project') */
|
|
126
|
+
referencedTable: string;
|
|
127
|
+
}
|
|
128
|
+
interface DependencyGraph {
|
|
129
|
+
/** Map of table -> tables it depends on (has FK references to) */
|
|
130
|
+
dependencies: Map<string, string[]>;
|
|
131
|
+
/** Map of table -> tables that depend on it (have FKs referencing this table) */
|
|
132
|
+
dependents: Map<string, string[]>;
|
|
133
|
+
/** Topologically sorted table names for optimal upload order */
|
|
134
|
+
uploadOrder: string[];
|
|
135
|
+
/** All detected FK relationships */
|
|
136
|
+
fkRelationships: FKDependency[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Convert a FK column name to its referenced table name
|
|
140
|
+
* Convention: columns ending in 'Id' reference the PascalCase table
|
|
141
|
+
* e.g., projectId -> Project, equipmentUnitId -> EquipmentUnit
|
|
142
|
+
*/
|
|
143
|
+
declare function fkColumnToTableName(columnName: string): string | null;
|
|
144
|
+
/**
|
|
145
|
+
* Detect FK dependencies from parsed tables
|
|
146
|
+
*
|
|
147
|
+
* Uses naming convention: columns ending in 'Id' reference the PascalCase table
|
|
148
|
+
* Only considers references to tables that are in the provided list (ignores external references)
|
|
149
|
+
*/
|
|
150
|
+
declare function detectFKDependencies(tables: ParsedTable[]): DependencyGraph;
|
|
151
|
+
/**
|
|
152
|
+
* Get optimal upload order using Kahn's topological sort algorithm
|
|
153
|
+
*
|
|
154
|
+
* Tables with no dependencies are uploaded first, then tables that depend on them, etc.
|
|
155
|
+
* This ensures that when a row references another table, the referenced row exists first.
|
|
156
|
+
*/
|
|
157
|
+
declare function getUploadOrder(graph: Pick<DependencyGraph, 'dependencies'>): string[];
|
|
158
|
+
/**
|
|
159
|
+
* Get tables that have no dependencies (root tables)
|
|
160
|
+
* These are typically reference/lookup tables like User, Project, etc.
|
|
161
|
+
*/
|
|
162
|
+
declare function getRootTables(graph: DependencyGraph): string[];
|
|
163
|
+
/**
|
|
164
|
+
* Get tables that have no dependents (leaf tables)
|
|
165
|
+
* These are typically transaction/event tables that reference other tables
|
|
166
|
+
*/
|
|
167
|
+
declare function getLeafTables(graph: DependencyGraph): string[];
|
|
168
|
+
/**
|
|
169
|
+
* Format the dependency graph as a human-readable string
|
|
170
|
+
*/
|
|
171
|
+
declare function formatDependencyGraph(graph: DependencyGraph): string;
|
|
172
|
+
|
|
102
173
|
/**
|
|
103
174
|
* PowerSync schema generator
|
|
104
175
|
*
|
|
@@ -118,6 +189,10 @@ interface GenerateResult {
|
|
|
118
189
|
warnings: string[];
|
|
119
190
|
/** Generated output (included when dryRun is true) */
|
|
120
191
|
output?: string;
|
|
192
|
+
/** Number of indexes generated */
|
|
193
|
+
indexesGenerated?: number;
|
|
194
|
+
/** FK dependency graph (when detected) */
|
|
195
|
+
dependencyGraph?: DependencyGraph;
|
|
121
196
|
}
|
|
122
197
|
/**
|
|
123
198
|
* Map TypeScript types to PowerSync column types
|
|
@@ -140,6 +215,20 @@ declare function generateSchema(config: GeneratorConfig, options?: {
|
|
|
140
215
|
* Output templates for PowerSync schema generation
|
|
141
216
|
*/
|
|
142
217
|
|
|
218
|
+
interface IndexDefinition {
|
|
219
|
+
name: string;
|
|
220
|
+
columns: string[];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Generate index definitions for a table based on column patterns
|
|
224
|
+
*
|
|
225
|
+
* @param tableName - The table name (PascalCase)
|
|
226
|
+
* @param columns - Array of column names in the table
|
|
227
|
+
* @param indexPatterns - Regex patterns to match against column names
|
|
228
|
+
* @param additionalColumns - Specific column names to always index
|
|
229
|
+
* @returns Array of index definitions
|
|
230
|
+
*/
|
|
231
|
+
declare function generateIndexDefinitions(tableName: string, columns: string[], indexPatterns: string[], additionalColumns?: string[]): IndexDefinition[];
|
|
143
232
|
/**
|
|
144
233
|
* File header template
|
|
145
234
|
*/
|
|
@@ -147,7 +236,7 @@ declare function generateHeader(typesPath: string): string;
|
|
|
147
236
|
/**
|
|
148
237
|
* Generate the table definition for a parsed table
|
|
149
238
|
*/
|
|
150
|
-
declare function generateTableDefinition(table: ParsedTable, columnDefs: string[]): string;
|
|
239
|
+
declare function generateTableDefinition(table: ParsedTable, columnDefs: string[], indexes?: IndexDefinition[]): string;
|
|
151
240
|
/**
|
|
152
241
|
* Generate the schema export section
|
|
153
242
|
*/
|
|
@@ -165,4 +254,4 @@ declare function generateFKUtility(): string;
|
|
|
165
254
|
*/
|
|
166
255
|
declare function generateOutputFile(tables: ParsedTable[], tableDefs: string[], schemas: string[], typesPath: string): string;
|
|
167
256
|
|
|
168
|
-
export { type ColumnInfo, type ColumnMapping, DEFAULT_DECIMAL_PATTERNS, DEFAULT_SKIP_COLUMNS, type GenerateResult, type GeneratorConfig, type ParseOptions, type ParsedTable, type TableConfig, defineConfig, extractTableDef, generateColumnDefs, generateFKUtility, generateHeader, generateOutputFile, generateSchema, generateSchemaExport, generateSchemaMapping, generateTableDefinition, getAvailableSchemas, getTablesInSchema, mapTypeToPowerSync, parseRowType, parseTypesFile };
|
|
257
|
+
export { type ColumnInfo, type ColumnMapping, DEFAULT_DECIMAL_PATTERNS, DEFAULT_INDEX_PATTERNS, DEFAULT_SKIP_COLUMNS, type DependencyGraph, type FKDependency, type GenerateResult, type GeneratorConfig, type IndexDefinition, type ParseOptions, type ParsedTable, type TableConfig, defineConfig, detectFKDependencies, extractTableDef, fkColumnToTableName, formatDependencyGraph, generateColumnDefs, generateFKUtility, generateHeader, generateIndexDefinitions, generateOutputFile, generateSchema, generateSchemaExport, generateSchemaMapping, generateTableDefinition, getAvailableSchemas, getLeafTables, getRootTables, getTablesInSchema, getUploadOrder, mapTypeToPowerSync, parseRowType, parseTypesFile };
|
package/dist/generator/index.js
CHANGED
|
@@ -9,6 +9,14 @@ var DEFAULT_SKIP_COLUMNS = [
|
|
|
9
9
|
"legacyId"
|
|
10
10
|
];
|
|
11
11
|
var DEFAULT_DECIMAL_PATTERNS = ["hours", "watts", "voltage", "rate", "amount", "price", "cost", "total"];
|
|
12
|
+
var DEFAULT_INDEX_PATTERNS = [
|
|
13
|
+
"Id$",
|
|
14
|
+
// FK columns ending in Id (e.g., projectId, userId)
|
|
15
|
+
"^createdAt$",
|
|
16
|
+
"^updatedAt$",
|
|
17
|
+
"^status$",
|
|
18
|
+
"^type$"
|
|
19
|
+
];
|
|
12
20
|
|
|
13
21
|
// src/generator/generator.ts
|
|
14
22
|
import * as fs from "fs";
|
|
@@ -95,6 +103,38 @@ function getTablesInSchema(content, schema) {
|
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
// src/generator/templates.ts
|
|
106
|
+
function toSnakeCase(str) {
|
|
107
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
function generateIndexDefinitions(tableName, columns, indexPatterns, additionalColumns = []) {
|
|
110
|
+
const indexes = [];
|
|
111
|
+
const snakeTableName = toSnakeCase(tableName);
|
|
112
|
+
const columnsToIndex = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const column of columns) {
|
|
114
|
+
if (column === "id") continue;
|
|
115
|
+
for (const pattern of indexPatterns) {
|
|
116
|
+
const regex = new RegExp(pattern);
|
|
117
|
+
if (regex.test(column)) {
|
|
118
|
+
columnsToIndex.add(column);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const column of additionalColumns) {
|
|
124
|
+
if (columns.includes(column) && column !== "id") {
|
|
125
|
+
columnsToIndex.add(column);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const column of columnsToIndex) {
|
|
129
|
+
const snakeColumn = toSnakeCase(column);
|
|
130
|
+
indexes.push({
|
|
131
|
+
name: `idx_${snakeTableName}_${snakeColumn}`,
|
|
132
|
+
columns: [column]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
indexes.sort((a, b) => a.name.localeCompare(b.name));
|
|
136
|
+
return indexes;
|
|
137
|
+
}
|
|
98
138
|
function generateHeader(typesPath) {
|
|
99
139
|
return `/**
|
|
100
140
|
* PowerSync Schema Definition
|
|
@@ -108,11 +148,30 @@ function generateHeader(typesPath) {
|
|
|
108
148
|
import { column, Schema, Table } from "@powersync/react-native";
|
|
109
149
|
`;
|
|
110
150
|
}
|
|
111
|
-
function
|
|
151
|
+
function formatIndexes(indexes) {
|
|
152
|
+
if (indexes.length === 0) return "";
|
|
153
|
+
const indexLines = indexes.map((idx) => {
|
|
154
|
+
const columnsStr = idx.columns.map((c) => `'${c}'`).join(", ");
|
|
155
|
+
return ` { name: '${idx.name}', columns: [${columnsStr}] },`;
|
|
156
|
+
});
|
|
157
|
+
return `indexes: [
|
|
158
|
+
${indexLines.join("\n")}
|
|
159
|
+
]`;
|
|
160
|
+
}
|
|
161
|
+
function generateTableDefinition(table, columnDefs, indexes = []) {
|
|
112
162
|
if (columnDefs.length === 0) {
|
|
113
163
|
return `// ${table.name} - no syncable columns found`;
|
|
114
164
|
}
|
|
115
|
-
const
|
|
165
|
+
const optionsParts = [];
|
|
166
|
+
if (table.config.trackMetadata) {
|
|
167
|
+
optionsParts.push("trackMetadata: true");
|
|
168
|
+
}
|
|
169
|
+
if (indexes.length > 0) {
|
|
170
|
+
optionsParts.push(formatIndexes(indexes));
|
|
171
|
+
}
|
|
172
|
+
const optionsStr = optionsParts.length > 0 ? `, {
|
|
173
|
+
${optionsParts.join(",\n ")}
|
|
174
|
+
}` : "";
|
|
116
175
|
return `const ${table.name} = new Table({
|
|
117
176
|
${columnDefs.join("\n")}
|
|
118
177
|
}${optionsStr});`;
|
|
@@ -224,6 +283,133 @@ ${generateFKUtility()}
|
|
|
224
283
|
`;
|
|
225
284
|
}
|
|
226
285
|
|
|
286
|
+
// src/generator/fk-dependencies.ts
|
|
287
|
+
function fkColumnToTableName(columnName) {
|
|
288
|
+
if (!columnName.endsWith("Id") || columnName === "id") {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const baseName = columnName.slice(0, -2);
|
|
292
|
+
return baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
293
|
+
}
|
|
294
|
+
function detectFKDependencies(tables) {
|
|
295
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
296
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
297
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
298
|
+
const fkRelationships = [];
|
|
299
|
+
for (const table of tables) {
|
|
300
|
+
dependencies.set(table.name, []);
|
|
301
|
+
dependents.set(table.name, []);
|
|
302
|
+
}
|
|
303
|
+
for (const table of tables) {
|
|
304
|
+
for (const [columnName] of table.columns) {
|
|
305
|
+
const referencedTable = fkColumnToTableName(columnName);
|
|
306
|
+
if (referencedTable && tableNames.has(referencedTable)) {
|
|
307
|
+
const tableDeps = dependencies.get(table.name) || [];
|
|
308
|
+
if (!tableDeps.includes(referencedTable)) {
|
|
309
|
+
tableDeps.push(referencedTable);
|
|
310
|
+
dependencies.set(table.name, tableDeps);
|
|
311
|
+
}
|
|
312
|
+
const refDeps = dependents.get(referencedTable) || [];
|
|
313
|
+
if (!refDeps.includes(table.name)) {
|
|
314
|
+
refDeps.push(table.name);
|
|
315
|
+
dependents.set(referencedTable, refDeps);
|
|
316
|
+
}
|
|
317
|
+
fkRelationships.push({
|
|
318
|
+
table: table.name,
|
|
319
|
+
column: columnName,
|
|
320
|
+
referencedTable
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const uploadOrder = getUploadOrder({
|
|
326
|
+
dependencies
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
dependencies,
|
|
330
|
+
dependents,
|
|
331
|
+
uploadOrder,
|
|
332
|
+
fkRelationships
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function getUploadOrder(graph) {
|
|
336
|
+
const result = [];
|
|
337
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
338
|
+
const adjList = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const [table, deps] of graph.dependencies) {
|
|
340
|
+
inDegree.set(table, deps.length);
|
|
341
|
+
adjList.set(table, []);
|
|
342
|
+
}
|
|
343
|
+
for (const [table, deps] of graph.dependencies) {
|
|
344
|
+
for (const dep of deps) {
|
|
345
|
+
const list = adjList.get(dep) || [];
|
|
346
|
+
list.push(table);
|
|
347
|
+
adjList.set(dep, list);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const queue = [];
|
|
351
|
+
for (const [table, degree] of inDegree) {
|
|
352
|
+
if (degree === 0) {
|
|
353
|
+
queue.push(table);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
queue.sort();
|
|
357
|
+
while (queue.length > 0) {
|
|
358
|
+
const table = queue.shift();
|
|
359
|
+
result.push(table);
|
|
360
|
+
const dependentTables = adjList.get(table) || [];
|
|
361
|
+
for (const dependent of dependentTables) {
|
|
362
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
363
|
+
inDegree.set(dependent, newDegree);
|
|
364
|
+
if (newDegree === 0) {
|
|
365
|
+
queue.push(dependent);
|
|
366
|
+
queue.sort();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (result.length < graph.dependencies.size) {
|
|
371
|
+
const remaining = [...graph.dependencies.keys()].filter((t) => !result.includes(t)).sort();
|
|
372
|
+
result.push(...remaining);
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
function getRootTables(graph) {
|
|
377
|
+
const roots = [];
|
|
378
|
+
for (const [table, deps] of graph.dependencies) {
|
|
379
|
+
if (deps.length === 0) {
|
|
380
|
+
roots.push(table);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return roots.sort();
|
|
384
|
+
}
|
|
385
|
+
function getLeafTables(graph) {
|
|
386
|
+
const leaves = [];
|
|
387
|
+
for (const [table, deps] of graph.dependents) {
|
|
388
|
+
if (deps.length === 0) {
|
|
389
|
+
leaves.push(table);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return leaves.sort();
|
|
393
|
+
}
|
|
394
|
+
function formatDependencyGraph(graph) {
|
|
395
|
+
const lines = [];
|
|
396
|
+
lines.push("FK Dependencies:");
|
|
397
|
+
for (const fk of graph.fkRelationships) {
|
|
398
|
+
lines.push(` ${fk.table}.${fk.column} -> ${fk.referencedTable}`);
|
|
399
|
+
}
|
|
400
|
+
if (graph.fkRelationships.length === 0) {
|
|
401
|
+
lines.push(" (none detected)");
|
|
402
|
+
}
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("Upload Order (topological):");
|
|
405
|
+
graph.uploadOrder.forEach((table, i) => {
|
|
406
|
+
const deps = graph.dependencies.get(table) || [];
|
|
407
|
+
const depStr = deps.length > 0 ? ` (depends on: ${deps.join(", ")})` : " (root)";
|
|
408
|
+
lines.push(` ${i + 1}. ${table}${depStr}`);
|
|
409
|
+
});
|
|
410
|
+
return lines.join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
227
413
|
// src/generator/generator.ts
|
|
228
414
|
function mapTypeToPowerSync(tsType, columnName, decimalPatterns) {
|
|
229
415
|
const cleanType = tsType.trim().replace(/\s*\|\s*null/g, "");
|
|
@@ -315,7 +501,26 @@ async function generateSchema(config, options) {
|
|
|
315
501
|
result.errors.push("No tables were parsed successfully");
|
|
316
502
|
return result;
|
|
317
503
|
}
|
|
504
|
+
const autoIndexes = config.autoIndexes ?? true;
|
|
505
|
+
const indexPatterns = autoIndexes ? [...DEFAULT_INDEX_PATTERNS, ...config.indexPatterns ?? []] : config.indexPatterns ?? [];
|
|
506
|
+
const indexColumns = config.indexColumns ?? [];
|
|
507
|
+
const dependencyGraph = detectFKDependencies(parsedTables);
|
|
508
|
+
result.dependencyGraph = dependencyGraph;
|
|
509
|
+
if (verbose && dependencyGraph.fkRelationships.length > 0) {
|
|
510
|
+
console.log(`
|
|
511
|
+
FK Dependencies detected: ${dependencyGraph.fkRelationships.length}`);
|
|
512
|
+
for (const fk of dependencyGraph.fkRelationships) {
|
|
513
|
+
console.log(` ${fk.table}.${fk.column} -> ${fk.referencedTable}`);
|
|
514
|
+
}
|
|
515
|
+
console.log(`
|
|
516
|
+
Recommended upload order:`);
|
|
517
|
+
dependencyGraph.uploadOrder.forEach((table, i) => {
|
|
518
|
+
console.log(` ${i + 1}. ${table}`);
|
|
519
|
+
});
|
|
520
|
+
console.log("");
|
|
521
|
+
}
|
|
318
522
|
const tableDefs = [];
|
|
523
|
+
let totalIndexes = 0;
|
|
319
524
|
for (const table of parsedTables) {
|
|
320
525
|
if (verbose) {
|
|
321
526
|
const syncPK = table.config.syncPrimaryKey || table.config.includeId;
|
|
@@ -326,8 +531,15 @@ async function generateSchema(config, options) {
|
|
|
326
531
|
result.warnings.push(`Table '${table.name}' has no syncable columns`);
|
|
327
532
|
continue;
|
|
328
533
|
}
|
|
329
|
-
|
|
534
|
+
const columnNames = [...table.columns.keys()];
|
|
535
|
+
const indexes = indexPatterns.length > 0 || indexColumns.length > 0 ? generateIndexDefinitions(table.name, columnNames, indexPatterns, indexColumns) : [];
|
|
536
|
+
totalIndexes += indexes.length;
|
|
537
|
+
if (verbose && indexes.length > 0) {
|
|
538
|
+
console.log(` Indexes: ${indexes.map((i) => i.name).join(", ")}`);
|
|
539
|
+
}
|
|
540
|
+
tableDefs.push(generateTableDefinition(table, columnDefs, indexes));
|
|
330
541
|
}
|
|
542
|
+
result.indexesGenerated = totalIndexes;
|
|
331
543
|
const schemas = [...new Set(config.tables.map((t) => t.schema ?? "public"))];
|
|
332
544
|
const relativePath = path.relative(cwd, typesPath);
|
|
333
545
|
const output = generateOutputFile(parsedTables.filter((t) => tableDefs.some((def) => def.includes(`const ${t.name} =`))), tableDefs, schemas, relativePath);
|
|
@@ -350,19 +562,27 @@ async function generateSchema(config, options) {
|
|
|
350
562
|
}
|
|
351
563
|
export {
|
|
352
564
|
DEFAULT_DECIMAL_PATTERNS,
|
|
565
|
+
DEFAULT_INDEX_PATTERNS,
|
|
353
566
|
DEFAULT_SKIP_COLUMNS,
|
|
354
567
|
defineConfig,
|
|
568
|
+
detectFKDependencies,
|
|
355
569
|
extractTableDef,
|
|
570
|
+
fkColumnToTableName,
|
|
571
|
+
formatDependencyGraph,
|
|
356
572
|
generateColumnDefs,
|
|
357
573
|
generateFKUtility,
|
|
358
574
|
generateHeader,
|
|
575
|
+
generateIndexDefinitions,
|
|
359
576
|
generateOutputFile,
|
|
360
577
|
generateSchema,
|
|
361
578
|
generateSchemaExport,
|
|
362
579
|
generateSchemaMapping,
|
|
363
580
|
generateTableDefinition,
|
|
364
581
|
getAvailableSchemas,
|
|
582
|
+
getLeafTables,
|
|
583
|
+
getRootTables,
|
|
365
584
|
getTablesInSchema,
|
|
585
|
+
getUploadOrder,
|
|
366
586
|
mapTypeToPowerSync,
|
|
367
587
|
parseRowType,
|
|
368
588
|
parseTypesFile
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/generator/config.ts","../../src/generator/generator.ts","../../src/generator/parser.ts","../../src/generator/templates.ts"],"sourcesContent":["/**\n * Configuration types and helpers for PowerSync schema generator\n */\n\nexport interface TableConfig {\n /** Table name (PascalCase as it appears in database.types.ts) */\n name: string;\n /** Schema name (defaults to 'public') */\n schema?: string;\n /** Enable ps_crud timestamp tracking for optimistic UI updates */\n trackMetadata?: boolean;\n /**\n * Sync the primary key column (normally skipped as PowerSync handles it internally).\n *\n * Use this for tables with integer PKs that are referenced by FKs in other tables.\n * Example: `Group.id` is referenced by `UserGroup.groupId`, so Group needs `syncPrimaryKey: true`\n * to ensure the integer ID is available for client-side joins.\n */\n syncPrimaryKey?: boolean;\n /** @deprecated Use `syncPrimaryKey` instead */\n includeId?: boolean;\n /** Columns to skip for this specific table (in addition to global skipColumns) */\n skipColumns?: string[];\n /** Only include these columns (overrides skipColumns if specified) */\n onlyColumns?: string[];\n}\nexport interface GeneratorConfig {\n /** Path to Supabase-generated database.types.ts file */\n typesPath: string;\n /** Output path for generated PowerSync schema */\n outputPath: string;\n /** Tables to include in the PowerSync schema */\n tables: TableConfig[];\n /** Columns to always skip (in addition to defaults like 'id') */\n skipColumns?: string[];\n /** Column name patterns that should use column.real for decimal values */\n decimalPatterns?: string[];\n /** Additional schemas to track (besides 'public' which is the default) */\n schemas?: string[];\n}\n\n/**\n * Define a PowerSync generator configuration with type safety\n */\nexport function defineConfig(config: GeneratorConfig): GeneratorConfig {\n return config;\n}\n\n/**\n * Default columns that are skipped during generation\n */\nexport const DEFAULT_SKIP_COLUMNS = ['id',\n// PowerSync handles id automatically\n// Legacy numeric ID columns - typically not needed after UUID migration\n'legacyId'];\n\n/**\n * Default column name patterns that indicate decimal values\n */\nexport const DEFAULT_DECIMAL_PATTERNS = ['hours', 'watts', 'voltage', 'rate', 'amount', 'price', 'cost', 'total'];","/**\n * PowerSync schema generator\n *\n * Converts Supabase database.types.ts into PowerSync schema definitions\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { GeneratorConfig } from './config.js';\nimport { DEFAULT_SKIP_COLUMNS, DEFAULT_DECIMAL_PATTERNS } from './config.js';\nimport { parseTypesFile, type ParsedTable } from './parser.js';\nimport { generateTableDefinition, generateOutputFile } from './templates.js';\nexport interface ColumnMapping {\n type: 'column.text' | 'column.integer' | 'column.real';\n isEnum?: boolean;\n isBoolean?: boolean;\n}\nexport interface GenerateResult {\n success: boolean;\n tablesGenerated: number;\n outputPath: string;\n errors: string[];\n warnings: string[];\n /** Generated output (included when dryRun is true) */\n output?: string;\n}\n\n/**\n * Map TypeScript types to PowerSync column types\n */\nexport function mapTypeToPowerSync(tsType: string, columnName: string, decimalPatterns: string[]): ColumnMapping | null {\n // Clean up the type (remove nullability)\n const cleanType = tsType.trim().replace(/\\s*\\|\\s*null/g, '');\n\n // Skip complex types that can't be stored in SQLite\n if (cleanType.includes('Json') || cleanType.includes('unknown') || cleanType.includes('{')) {\n return null;\n }\n\n // Array types - skip\n if (cleanType.includes('[]')) {\n return null;\n }\n\n // Boolean -> integer (0/1)\n if (cleanType === 'boolean') {\n return {\n type: 'column.integer',\n isBoolean: true\n };\n }\n\n // Number types\n if (cleanType === 'number') {\n // Use real for columns that might have decimals\n if (decimalPatterns.some(pattern => columnName.toLowerCase().includes(pattern.toLowerCase()))) {\n return {\n type: 'column.real'\n };\n }\n return {\n type: 'column.integer'\n };\n }\n\n // String types\n if (cleanType === 'string') {\n return {\n type: 'column.text'\n };\n }\n\n // Enum types (Database[\"schema\"][\"Enums\"][\"EnumName\"]) -> store as text\n if (cleanType.includes('Database[') && cleanType.includes('Enums')) {\n return {\n type: 'column.text',\n isEnum: true\n };\n }\n\n // Default to text for unknown types (likely enums or other string-like types)\n return {\n type: 'column.text'\n };\n}\n\n/**\n * Generate column definitions for a table\n */\nexport function generateColumnDefs(table: ParsedTable, decimalPatterns: string[]): string[] {\n const columnDefs: string[] = [];\n for (const [columnName, tsType] of table.columns) {\n const mapping = mapTypeToPowerSync(tsType, columnName, decimalPatterns);\n if (mapping) {\n // Add comment for boolean and enum columns\n let comment = '';\n if (mapping.isBoolean) {\n comment = ' // boolean stored as 0/1';\n } else if (mapping.isEnum) {\n comment = ' // enum stored as text';\n }\n columnDefs.push(` ${columnName}: ${mapping.type},${comment}`);\n }\n }\n return columnDefs;\n}\n\n/**\n * Generate PowerSync schema from configuration\n */\nexport async function generateSchema(config: GeneratorConfig, options?: {\n cwd?: string;\n verbose?: boolean;\n dryRun?: boolean;\n}): Promise<GenerateResult> {\n const cwd = options?.cwd ?? process.cwd();\n const verbose = options?.verbose ?? false;\n const dryRun = options?.dryRun ?? false;\n const result: GenerateResult = {\n success: false,\n tablesGenerated: 0,\n outputPath: '',\n errors: [],\n warnings: []\n };\n\n // Resolve paths relative to cwd\n const typesPath = path.isAbsolute(config.typesPath) ? config.typesPath : path.resolve(cwd, config.typesPath);\n const outputPath = path.isAbsolute(config.outputPath) ? config.outputPath : path.resolve(cwd, config.outputPath);\n result.outputPath = outputPath;\n\n // Check if types file exists\n if (!fs.existsSync(typesPath)) {\n result.errors.push(`Types file not found: ${typesPath}`);\n return result;\n }\n\n // Read types file\n if (verbose) {\n console.log(`Reading types from: ${typesPath}`);\n }\n const typesContent = fs.readFileSync(typesPath, 'utf-8');\n\n // Build skip columns set\n const skipColumns = new Set([...DEFAULT_SKIP_COLUMNS, ...(config.skipColumns ?? [])]);\n\n // Build decimal patterns\n const decimalPatterns = [...DEFAULT_DECIMAL_PATTERNS, ...(config.decimalPatterns ?? [])];\n\n // Parse tables from types file\n const parsedTables = parseTypesFile(typesContent, config.tables, skipColumns);\n\n // Check for tables that weren't found\n for (const tableConfig of config.tables) {\n const found = parsedTables.some(t => t.name === tableConfig.name);\n if (!found) {\n result.warnings.push(`Table '${tableConfig.name}' not found in schema '${tableConfig.schema ?? 'public'}'`);\n }\n }\n if (parsedTables.length === 0) {\n result.errors.push('No tables were parsed successfully');\n return result;\n }\n\n // Generate table definitions\n const tableDefs: string[] = [];\n for (const table of parsedTables) {\n if (verbose) {\n const syncPK = table.config.syncPrimaryKey || table.config.includeId;\n console.log(`Processing ${table.schema}.${table.name} (${table.columns.size} columns)${table.config.trackMetadata ? ' [trackMetadata]' : ''}${syncPK ? ' [syncPrimaryKey]' : ''}`);\n }\n const columnDefs = generateColumnDefs(table, decimalPatterns);\n if (columnDefs.length === 0) {\n result.warnings.push(`Table '${table.name}' has no syncable columns`);\n continue;\n }\n tableDefs.push(generateTableDefinition(table, columnDefs));\n }\n\n // Collect unique schemas\n const schemas = [...new Set(config.tables.map(t => t.schema ?? 'public'))];\n\n // Generate output file content\n const relativePath = path.relative(cwd, typesPath);\n const output = generateOutputFile(parsedTables.filter(t => tableDefs.some(def => def.includes(`const ${t.name} =`))), tableDefs, schemas, relativePath);\n\n // If dry-run, return output without writing\n if (dryRun) {\n result.success = true;\n result.tablesGenerated = tableDefs.length;\n result.output = output;\n return result;\n }\n\n // Ensure output directory exists\n const outputDir = path.dirname(outputPath);\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, {\n recursive: true\n });\n }\n\n // Write output file\n fs.writeFileSync(outputPath, output);\n result.success = true;\n result.tablesGenerated = tableDefs.length;\n return result;\n}","/**\n * Parser for Supabase database.types.ts files\n *\n * Extracts table definitions and column types from the generated TypeScript types\n */\n\nimport type { TableConfig } from './config.js';\nexport interface ColumnInfo {\n name: string;\n tsType: string;\n isNullable: boolean;\n}\nexport interface ParsedTable {\n name: string;\n schema: string;\n columns: Map<string, string>;\n config: TableConfig;\n}\nexport interface ParseOptions {\n /** Columns to skip */\n skipColumns: Set<string>;\n /**\n * Include the id column (normally skipped).\n * Use for tables with integer PKs referenced by FKs in other tables.\n */\n syncPrimaryKey?: boolean;\n /** @deprecated Use `syncPrimaryKey` instead */\n includeId?: boolean;\n}\n\n/**\n * Parse the Row type from a table definition and extract columns\n */\nexport function parseRowType(tableContent: string, options: ParseOptions): Map<string, string> {\n const columns = new Map<string, string>();\n\n // Find the Row block - handles nested braces in type definitions\n const rowMatch = tableContent.match(/Row:\\s*\\{([^}]+(?:\\{[^}]*\\}[^}]*)*)\\}/s);\n if (!rowMatch) return columns;\n const rowContent = rowMatch[1];\n\n // syncPrimaryKey takes precedence, with includeId as fallback for backwards compat\n const includePrimaryKey = options.syncPrimaryKey ?? options.includeId ?? false;\n\n // Parse each column: \"columnName: type\" or \"columnName?: type\"\n const columnRegex = /(\\w+)\\??:\\s*([^,\\n]+)/g;\n let match;\n while ((match = columnRegex.exec(rowContent)) !== null) {\n const [, columnName, columnType] = match;\n // Skip columns unless syncPrimaryKey is true for id column\n const shouldSkip = options.skipColumns.has(columnName) && !(includePrimaryKey && columnName === 'id');\n if (!shouldSkip) {\n columns.set(columnName, columnType.trim());\n }\n }\n return columns;\n}\n\n/**\n * Extract a table definition from the database.types.ts content\n */\nexport function extractTableDef(content: string, tableName: string, schema: string): string | null {\n // Find the schema section\n const schemaRegex = new RegExp(`${schema}:\\\\s*\\\\{[\\\\s\\\\S]*?Tables:\\\\s*\\\\{`, 'g');\n const schemaMatch = schemaRegex.exec(content);\n if (!schemaMatch) return null;\n const startIndex = schemaMatch.index;\n\n // Find this specific table within the schema\n // Use negative lookbehind (?<![A-Za-z]) to avoid matching table names that are\n // substrings of other names (e.g., \"Tag\" in \"CommentTag\")\n const tableRegex = new RegExp(`(?<![A-Za-z])${tableName}:\\\\s*\\\\{[\\\\s\\\\S]*?Row:\\\\s*\\\\{[\\\\s\\\\S]*?\\\\}[\\\\s\\\\S]*?Relationships:\\\\s*\\\\[[^\\\\]]*\\\\]\\\\s*\\\\}`, 'g');\n\n // Search from the schema start\n const searchContent = content.slice(startIndex);\n const tableMatch = tableRegex.exec(searchContent);\n return tableMatch ? tableMatch[0] : null;\n}\n\n/**\n * Parse a database.types.ts file and extract specified tables\n */\nexport function parseTypesFile(content: string, tables: TableConfig[], skipColumns: Set<string>): ParsedTable[] {\n const parsedTables: ParsedTable[] = [];\n for (const tableConfig of tables) {\n const {\n name,\n schema = 'public',\n syncPrimaryKey,\n includeId\n } = tableConfig;\n const tableDef = extractTableDef(content, name, schema);\n if (!tableDef) {\n continue;\n }\n const columns = parseRowType(tableDef, {\n skipColumns,\n syncPrimaryKey,\n includeId\n });\n if (columns.size > 0) {\n parsedTables.push({\n name,\n schema,\n columns,\n config: tableConfig\n });\n }\n }\n return parsedTables;\n}\n\n/**\n * Get all available schemas from the types file\n */\nexport function getAvailableSchemas(content: string): string[] {\n const schemas: string[] = [];\n const schemaRegex = /(\\w+):\\s*\\{[\\s\\S]*?Tables:\\s*\\{/g;\n let match;\n while ((match = schemaRegex.exec(content)) !== null) {\n schemas.push(match[1]);\n }\n return schemas;\n}\n\n/**\n * Get all table names in a schema\n */\nexport function getTablesInSchema(content: string, schema: string): string[] {\n const tables: string[] = [];\n\n // Find the schema section\n const schemaRegex = new RegExp(`${schema}:\\\\s*\\\\{[\\\\s\\\\S]*?Tables:\\\\s*\\\\{([\\\\s\\\\S]*?)\\\\}\\\\s*Views:`, 'g');\n const schemaMatch = schemaRegex.exec(content);\n if (!schemaMatch) return tables;\n const tablesContent = schemaMatch[1];\n\n // Find table names (they're at the start of each table definition)\n const tableNameRegex = /^\\s*(\\w+):\\s*\\{/gm;\n let match;\n while ((match = tableNameRegex.exec(tablesContent)) !== null) {\n tables.push(match[1]);\n }\n return tables;\n}","/**\n * Output templates for PowerSync schema generation\n */\n\nimport type { ParsedTable } from './parser.js';\n\n/**\n * File header template\n */\nexport function generateHeader(typesPath: string): string {\n return `/**\n * PowerSync Schema Definition\n *\n * AUTO-GENERATED from ${typesPath}\n * Run: npx @pol-studios/powersync generate-schema\n *\n * DO NOT EDIT MANUALLY - changes will be overwritten\n */\n\nimport { column, Schema, Table } from \"@powersync/react-native\";\n`;\n}\n\n/**\n * Generate the table definition for a parsed table\n */\nexport function generateTableDefinition(table: ParsedTable, columnDefs: string[]): string {\n if (columnDefs.length === 0) {\n return `// ${table.name} - no syncable columns found`;\n }\n const optionsStr = table.config.trackMetadata ? ', { trackMetadata: true }' : '';\n return `const ${table.name} = new Table({\n${columnDefs.join('\\n')}\n}${optionsStr});`;\n}\n\n/**\n * Generate the schema export section\n */\nexport function generateSchemaExport(tableNames: string[]): string {\n return `// ============================================================================\n// SCHEMA EXPORT\n// ============================================================================\n\n// NOTE: photo_attachments is NOT included here.\n// The AttachmentQueue from @powersync/attachments creates and manages\n// its own internal SQLite table (not a view) during queue.init().\n// This allows INSERT/UPDATE operations to work correctly.\n\nexport const AppSchema = new Schema({\n${tableNames.map(name => ` ${name},`).join('\\n')}\n});\n\nexport type Database = (typeof AppSchema)[\"types\"];`;\n}\n\n/**\n * Generate schema mapping utilities\n */\nexport function generateSchemaMapping(tables: ParsedTable[], schemas: string[]): string {\n // Group tables by non-public schemas\n const schemaGroups = new Map<string, string[]>();\n for (const schema of schemas) {\n if (schema !== 'public') {\n schemaGroups.set(schema, []);\n }\n }\n for (const table of tables) {\n if (table.schema !== 'public' && schemaGroups.has(table.schema)) {\n schemaGroups.get(table.schema)!.push(table.name);\n }\n }\n const sections: string[] = [`// ============================================================================\n// SCHEMA MAPPING FOR CONNECTOR\n// ============================================================================`];\n\n // Generate constants for each non-public schema\n for (const [schema, tableNames] of schemaGroups) {\n if (tableNames.length > 0) {\n const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;\n sections.push(`\n// Tables in the '${schema}' schema (need .schema('${schema}') in Supabase queries)\nexport const ${constName} = new Set([\n${tableNames.map(name => ` \"${name}\",`).join('\\n')}\n]);`);\n }\n }\n\n // Generate helper function\n const schemaChecks = Array.from(schemaGroups.entries()).filter(([, names]) => names.length > 0).map(([schema]) => {\n const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;\n return ` if (${constName}.has(tableName)) return \"${schema}\";`;\n });\n if (schemaChecks.length > 0) {\n sections.push(`\n/**\n * Get the Supabase schema for a table\n */\nexport function getTableSchema(tableName: string): ${schemas.map(s => `\"${s}\"`).join(' | ')} {\n${schemaChecks.join('\\n')}\n return \"public\";\n}`);\n } else {\n sections.push(`\n/**\n * Get the Supabase schema for a table\n */\nexport function getTableSchema(tableName: string): \"public\" {\n return \"public\";\n}`);\n }\n return sections.join('\\n');\n}\n\n/**\n * Generate the FK detection utility (helpful for consumers)\n */\nexport function generateFKUtility(): string {\n return `\n// ============================================================================\n// FOREIGN KEY UTILITIES\n// ============================================================================\n\n/**\n * Check if a column name represents a foreign key reference\n * Convention: columns ending in 'Id' are foreign keys (e.g., projectId -> Project table)\n */\nexport function isForeignKeyColumn(columnName: string): boolean {\n return columnName.endsWith('Id') && columnName !== 'id';\n}\n\n/**\n * Get the referenced table name from a foreign key column\n * e.g., 'projectId' -> 'Project', 'equipmentFixtureUnitId' -> 'EquipmentFixtureUnit'\n */\nexport function getForeignKeyTable(columnName: string): string | null {\n if (!isForeignKeyColumn(columnName)) return null;\n // Remove 'Id' suffix and capitalize first letter\n const baseName = columnName.slice(0, -2);\n return baseName.charAt(0).toUpperCase() + baseName.slice(1);\n}`;\n}\n\n/**\n * Generate complete output file\n */\nexport function generateOutputFile(tables: ParsedTable[], tableDefs: string[], schemas: string[], typesPath: string): string {\n const tableNames = tables.map(t => t.name);\n return `${generateHeader(typesPath)}\n\n// ============================================================================\n// TABLE DEFINITIONS\n// ============================================================================\n\n${tableDefs.join('\\n\\n')}\n\n${generateSchemaExport(tableNames)}\n\n${generateSchemaMapping(tables, ['public', ...schemas.filter(s => s !== 'public')])}\n${generateFKUtility()}\n`;\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/generator/config.ts","../../src/generator/generator.ts","../../src/generator/parser.ts","../../src/generator/templates.ts","../../src/generator/fk-dependencies.ts"],"sourcesContent":["/**\n * Configuration types and helpers for PowerSync schema generator\n */\n\nexport interface TableConfig {\n /** Table name (PascalCase as it appears in database.types.ts) */\n name: string;\n /** Schema name (defaults to 'public') */\n schema?: string;\n /** Enable ps_crud timestamp tracking for optimistic UI updates */\n trackMetadata?: boolean;\n /**\n * Sync the primary key column (normally skipped as PowerSync handles it internally).\n *\n * Use this for tables with integer PKs that are referenced by FKs in other tables.\n * Example: `Group.id` is referenced by `UserGroup.groupId`, so Group needs `syncPrimaryKey: true`\n * to ensure the integer ID is available for client-side joins.\n */\n syncPrimaryKey?: boolean;\n /** @deprecated Use `syncPrimaryKey` instead */\n includeId?: boolean;\n /** Columns to skip for this specific table (in addition to global skipColumns) */\n skipColumns?: string[];\n /** Only include these columns (overrides skipColumns if specified) */\n onlyColumns?: string[];\n}\nexport interface GeneratorConfig {\n /** Path to Supabase-generated database.types.ts file */\n typesPath: string;\n /** Output path for generated PowerSync schema */\n outputPath: string;\n /** Tables to include in the PowerSync schema */\n tables: TableConfig[];\n /** Columns to always skip (in addition to defaults like 'id') */\n skipColumns?: string[];\n /** Column name patterns that should use column.real for decimal values */\n decimalPatterns?: string[];\n /** Additional schemas to track (besides 'public' which is the default) */\n schemas?: string[];\n /** Generate indexes for FK and common columns (default: true) */\n autoIndexes?: boolean;\n /** Additional columns to index (exact column names) */\n indexColumns?: string[];\n /** Column patterns that should be indexed (regex patterns) */\n indexPatterns?: string[];\n}\n\n/**\n * Define a PowerSync generator configuration with type safety\n */\nexport function defineConfig(config: GeneratorConfig): GeneratorConfig {\n return config;\n}\n\n/**\n * Default columns that are skipped during generation\n */\nexport const DEFAULT_SKIP_COLUMNS = ['id',\n// PowerSync handles id automatically\n// Legacy numeric ID columns - typically not needed after UUID migration\n'legacyId'];\n\n/**\n * Default column name patterns that indicate decimal values\n */\nexport const DEFAULT_DECIMAL_PATTERNS = ['hours', 'watts', 'voltage', 'rate', 'amount', 'price', 'cost', 'total'];\n\n/**\n * Default column patterns that should be indexed\n * These are regex patterns matched against column names\n */\nexport const DEFAULT_INDEX_PATTERNS = ['Id$',\n// FK columns ending in Id (e.g., projectId, userId)\n'^createdAt$', '^updatedAt$', '^status$', '^type$'];","/**\n * PowerSync schema generator\n *\n * Converts Supabase database.types.ts into PowerSync schema definitions\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { GeneratorConfig } from './config.js';\nimport { DEFAULT_SKIP_COLUMNS, DEFAULT_DECIMAL_PATTERNS, DEFAULT_INDEX_PATTERNS } from './config.js';\nimport { parseTypesFile, type ParsedTable } from './parser.js';\nimport { generateTableDefinition, generateOutputFile, generateIndexDefinitions, type IndexDefinition } from './templates.js';\nimport { detectFKDependencies, type DependencyGraph } from './fk-dependencies.js';\nexport interface ColumnMapping {\n type: 'column.text' | 'column.integer' | 'column.real';\n isEnum?: boolean;\n isBoolean?: boolean;\n}\nexport interface GenerateResult {\n success: boolean;\n tablesGenerated: number;\n outputPath: string;\n errors: string[];\n warnings: string[];\n /** Generated output (included when dryRun is true) */\n output?: string;\n /** Number of indexes generated */\n indexesGenerated?: number;\n /** FK dependency graph (when detected) */\n dependencyGraph?: DependencyGraph;\n}\n\n/**\n * Map TypeScript types to PowerSync column types\n */\nexport function mapTypeToPowerSync(tsType: string, columnName: string, decimalPatterns: string[]): ColumnMapping | null {\n // Clean up the type (remove nullability)\n const cleanType = tsType.trim().replace(/\\s*\\|\\s*null/g, '');\n\n // Skip complex types that can't be stored in SQLite\n if (cleanType.includes('Json') || cleanType.includes('unknown') || cleanType.includes('{')) {\n return null;\n }\n\n // Array types - skip\n if (cleanType.includes('[]')) {\n return null;\n }\n\n // Boolean -> integer (0/1)\n if (cleanType === 'boolean') {\n return {\n type: 'column.integer',\n isBoolean: true\n };\n }\n\n // Number types\n if (cleanType === 'number') {\n // Use real for columns that might have decimals\n if (decimalPatterns.some(pattern => columnName.toLowerCase().includes(pattern.toLowerCase()))) {\n return {\n type: 'column.real'\n };\n }\n return {\n type: 'column.integer'\n };\n }\n\n // String types\n if (cleanType === 'string') {\n return {\n type: 'column.text'\n };\n }\n\n // Enum types (Database[\"schema\"][\"Enums\"][\"EnumName\"]) -> store as text\n if (cleanType.includes('Database[') && cleanType.includes('Enums')) {\n return {\n type: 'column.text',\n isEnum: true\n };\n }\n\n // Default to text for unknown types (likely enums or other string-like types)\n return {\n type: 'column.text'\n };\n}\n\n/**\n * Generate column definitions for a table\n */\nexport function generateColumnDefs(table: ParsedTable, decimalPatterns: string[]): string[] {\n const columnDefs: string[] = [];\n for (const [columnName, tsType] of table.columns) {\n const mapping = mapTypeToPowerSync(tsType, columnName, decimalPatterns);\n if (mapping) {\n // Add comment for boolean and enum columns\n let comment = '';\n if (mapping.isBoolean) {\n comment = ' // boolean stored as 0/1';\n } else if (mapping.isEnum) {\n comment = ' // enum stored as text';\n }\n columnDefs.push(` ${columnName}: ${mapping.type},${comment}`);\n }\n }\n return columnDefs;\n}\n\n/**\n * Generate PowerSync schema from configuration\n */\nexport async function generateSchema(config: GeneratorConfig, options?: {\n cwd?: string;\n verbose?: boolean;\n dryRun?: boolean;\n}): Promise<GenerateResult> {\n const cwd = options?.cwd ?? process.cwd();\n const verbose = options?.verbose ?? false;\n const dryRun = options?.dryRun ?? false;\n const result: GenerateResult = {\n success: false,\n tablesGenerated: 0,\n outputPath: '',\n errors: [],\n warnings: []\n };\n\n // Resolve paths relative to cwd\n const typesPath = path.isAbsolute(config.typesPath) ? config.typesPath : path.resolve(cwd, config.typesPath);\n const outputPath = path.isAbsolute(config.outputPath) ? config.outputPath : path.resolve(cwd, config.outputPath);\n result.outputPath = outputPath;\n\n // Check if types file exists\n if (!fs.existsSync(typesPath)) {\n result.errors.push(`Types file not found: ${typesPath}`);\n return result;\n }\n\n // Read types file\n if (verbose) {\n console.log(`Reading types from: ${typesPath}`);\n }\n const typesContent = fs.readFileSync(typesPath, 'utf-8');\n\n // Build skip columns set\n const skipColumns = new Set([...DEFAULT_SKIP_COLUMNS, ...(config.skipColumns ?? [])]);\n\n // Build decimal patterns\n const decimalPatterns = [...DEFAULT_DECIMAL_PATTERNS, ...(config.decimalPatterns ?? [])];\n\n // Parse tables from types file\n const parsedTables = parseTypesFile(typesContent, config.tables, skipColumns);\n\n // Check for tables that weren't found\n for (const tableConfig of config.tables) {\n const found = parsedTables.some(t => t.name === tableConfig.name);\n if (!found) {\n result.warnings.push(`Table '${tableConfig.name}' not found in schema '${tableConfig.schema ?? 'public'}'`);\n }\n }\n if (parsedTables.length === 0) {\n result.errors.push('No tables were parsed successfully');\n return result;\n }\n\n // Build index patterns (auto-indexes enabled by default)\n const autoIndexes = config.autoIndexes ?? true;\n const indexPatterns = autoIndexes ? [...DEFAULT_INDEX_PATTERNS, ...(config.indexPatterns ?? [])] : config.indexPatterns ?? [];\n const indexColumns = config.indexColumns ?? [];\n\n // Detect FK dependencies\n const dependencyGraph = detectFKDependencies(parsedTables);\n result.dependencyGraph = dependencyGraph;\n if (verbose && dependencyGraph.fkRelationships.length > 0) {\n console.log(`\\nFK Dependencies detected: ${dependencyGraph.fkRelationships.length}`);\n for (const fk of dependencyGraph.fkRelationships) {\n console.log(` ${fk.table}.${fk.column} -> ${fk.referencedTable}`);\n }\n console.log(`\\nRecommended upload order:`);\n dependencyGraph.uploadOrder.forEach((table, i) => {\n console.log(` ${i + 1}. ${table}`);\n });\n console.log('');\n }\n\n // Generate table definitions\n const tableDefs: string[] = [];\n let totalIndexes = 0;\n for (const table of parsedTables) {\n if (verbose) {\n const syncPK = table.config.syncPrimaryKey || table.config.includeId;\n console.log(`Processing ${table.schema}.${table.name} (${table.columns.size} columns)${table.config.trackMetadata ? ' [trackMetadata]' : ''}${syncPK ? ' [syncPrimaryKey]' : ''}`);\n }\n const columnDefs = generateColumnDefs(table, decimalPatterns);\n if (columnDefs.length === 0) {\n result.warnings.push(`Table '${table.name}' has no syncable columns`);\n continue;\n }\n\n // Generate indexes for this table\n const columnNames = [...table.columns.keys()];\n const indexes = indexPatterns.length > 0 || indexColumns.length > 0 ? generateIndexDefinitions(table.name, columnNames, indexPatterns, indexColumns) : [];\n totalIndexes += indexes.length;\n if (verbose && indexes.length > 0) {\n console.log(` Indexes: ${indexes.map(i => i.name).join(', ')}`);\n }\n tableDefs.push(generateTableDefinition(table, columnDefs, indexes));\n }\n result.indexesGenerated = totalIndexes;\n\n // Collect unique schemas\n const schemas = [...new Set(config.tables.map(t => t.schema ?? 'public'))];\n\n // Generate output file content\n const relativePath = path.relative(cwd, typesPath);\n const output = generateOutputFile(parsedTables.filter(t => tableDefs.some(def => def.includes(`const ${t.name} =`))), tableDefs, schemas, relativePath);\n\n // If dry-run, return output without writing\n if (dryRun) {\n result.success = true;\n result.tablesGenerated = tableDefs.length;\n result.output = output;\n return result;\n }\n\n // Ensure output directory exists\n const outputDir = path.dirname(outputPath);\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, {\n recursive: true\n });\n }\n\n // Write output file\n fs.writeFileSync(outputPath, output);\n result.success = true;\n result.tablesGenerated = tableDefs.length;\n return result;\n}","/**\n * Parser for Supabase database.types.ts files\n *\n * Extracts table definitions and column types from the generated TypeScript types\n */\n\nimport type { TableConfig } from './config.js';\nexport interface ColumnInfo {\n name: string;\n tsType: string;\n isNullable: boolean;\n}\nexport interface ParsedTable {\n name: string;\n schema: string;\n columns: Map<string, string>;\n config: TableConfig;\n}\nexport interface ParseOptions {\n /** Columns to skip */\n skipColumns: Set<string>;\n /**\n * Include the id column (normally skipped).\n * Use for tables with integer PKs referenced by FKs in other tables.\n */\n syncPrimaryKey?: boolean;\n /** @deprecated Use `syncPrimaryKey` instead */\n includeId?: boolean;\n}\n\n/**\n * Parse the Row type from a table definition and extract columns\n */\nexport function parseRowType(tableContent: string, options: ParseOptions): Map<string, string> {\n const columns = new Map<string, string>();\n\n // Find the Row block - handles nested braces in type definitions\n const rowMatch = tableContent.match(/Row:\\s*\\{([^}]+(?:\\{[^}]*\\}[^}]*)*)\\}/s);\n if (!rowMatch) return columns;\n const rowContent = rowMatch[1];\n\n // syncPrimaryKey takes precedence, with includeId as fallback for backwards compat\n const includePrimaryKey = options.syncPrimaryKey ?? options.includeId ?? false;\n\n // Parse each column: \"columnName: type\" or \"columnName?: type\"\n const columnRegex = /(\\w+)\\??:\\s*([^,\\n]+)/g;\n let match;\n while ((match = columnRegex.exec(rowContent)) !== null) {\n const [, columnName, columnType] = match;\n // Skip columns unless syncPrimaryKey is true for id column\n const shouldSkip = options.skipColumns.has(columnName) && !(includePrimaryKey && columnName === 'id');\n if (!shouldSkip) {\n columns.set(columnName, columnType.trim());\n }\n }\n return columns;\n}\n\n/**\n * Extract a table definition from the database.types.ts content\n */\nexport function extractTableDef(content: string, tableName: string, schema: string): string | null {\n // Find the schema section\n const schemaRegex = new RegExp(`${schema}:\\\\s*\\\\{[\\\\s\\\\S]*?Tables:\\\\s*\\\\{`, 'g');\n const schemaMatch = schemaRegex.exec(content);\n if (!schemaMatch) return null;\n const startIndex = schemaMatch.index;\n\n // Find this specific table within the schema\n // Use negative lookbehind (?<![A-Za-z]) to avoid matching table names that are\n // substrings of other names (e.g., \"Tag\" in \"CommentTag\")\n const tableRegex = new RegExp(`(?<![A-Za-z])${tableName}:\\\\s*\\\\{[\\\\s\\\\S]*?Row:\\\\s*\\\\{[\\\\s\\\\S]*?\\\\}[\\\\s\\\\S]*?Relationships:\\\\s*\\\\[[^\\\\]]*\\\\]\\\\s*\\\\}`, 'g');\n\n // Search from the schema start\n const searchContent = content.slice(startIndex);\n const tableMatch = tableRegex.exec(searchContent);\n return tableMatch ? tableMatch[0] : null;\n}\n\n/**\n * Parse a database.types.ts file and extract specified tables\n */\nexport function parseTypesFile(content: string, tables: TableConfig[], skipColumns: Set<string>): ParsedTable[] {\n const parsedTables: ParsedTable[] = [];\n for (const tableConfig of tables) {\n const {\n name,\n schema = 'public',\n syncPrimaryKey,\n includeId\n } = tableConfig;\n const tableDef = extractTableDef(content, name, schema);\n if (!tableDef) {\n continue;\n }\n const columns = parseRowType(tableDef, {\n skipColumns,\n syncPrimaryKey,\n includeId\n });\n if (columns.size > 0) {\n parsedTables.push({\n name,\n schema,\n columns,\n config: tableConfig\n });\n }\n }\n return parsedTables;\n}\n\n/**\n * Get all available schemas from the types file\n */\nexport function getAvailableSchemas(content: string): string[] {\n const schemas: string[] = [];\n const schemaRegex = /(\\w+):\\s*\\{[\\s\\S]*?Tables:\\s*\\{/g;\n let match;\n while ((match = schemaRegex.exec(content)) !== null) {\n schemas.push(match[1]);\n }\n return schemas;\n}\n\n/**\n * Get all table names in a schema\n */\nexport function getTablesInSchema(content: string, schema: string): string[] {\n const tables: string[] = [];\n\n // Find the schema section\n const schemaRegex = new RegExp(`${schema}:\\\\s*\\\\{[\\\\s\\\\S]*?Tables:\\\\s*\\\\{([\\\\s\\\\S]*?)\\\\}\\\\s*Views:`, 'g');\n const schemaMatch = schemaRegex.exec(content);\n if (!schemaMatch) return tables;\n const tablesContent = schemaMatch[1];\n\n // Find table names (they're at the start of each table definition)\n const tableNameRegex = /^\\s*(\\w+):\\s*\\{/gm;\n let match;\n while ((match = tableNameRegex.exec(tablesContent)) !== null) {\n tables.push(match[1]);\n }\n return tables;\n}","/**\n * Output templates for PowerSync schema generation\n */\n\nimport type { ParsedTable } from './parser.js';\nexport interface IndexDefinition {\n name: string;\n columns: string[];\n}\n\n/**\n * Convert table name to snake_case for index naming\n */\nfunction toSnakeCase(str: string): string {\n return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();\n}\n\n/**\n * Generate index definitions for a table based on column patterns\n *\n * @param tableName - The table name (PascalCase)\n * @param columns - Array of column names in the table\n * @param indexPatterns - Regex patterns to match against column names\n * @param additionalColumns - Specific column names to always index\n * @returns Array of index definitions\n */\nexport function generateIndexDefinitions(tableName: string, columns: string[], indexPatterns: string[], additionalColumns: string[] = []): IndexDefinition[] {\n const indexes: IndexDefinition[] = [];\n const snakeTableName = toSnakeCase(tableName);\n\n // Combine pattern matching and explicit columns\n const columnsToIndex = new Set<string>();\n\n // Match columns against patterns\n for (const column of columns) {\n // Skip 'id' column - it's the primary key and already indexed\n if (column === 'id') continue;\n for (const pattern of indexPatterns) {\n const regex = new RegExp(pattern);\n if (regex.test(column)) {\n columnsToIndex.add(column);\n break;\n }\n }\n }\n\n // Add explicitly specified columns (if they exist in the table)\n for (const column of additionalColumns) {\n if (columns.includes(column) && column !== 'id') {\n columnsToIndex.add(column);\n }\n }\n\n // Create index definitions\n for (const column of columnsToIndex) {\n const snakeColumn = toSnakeCase(column);\n indexes.push({\n name: `idx_${snakeTableName}_${snakeColumn}`,\n columns: [column]\n });\n }\n\n // Sort by index name for deterministic output\n indexes.sort((a, b) => a.name.localeCompare(b.name));\n return indexes;\n}\n\n/**\n * File header template\n */\nexport function generateHeader(typesPath: string): string {\n return `/**\n * PowerSync Schema Definition\n *\n * AUTO-GENERATED from ${typesPath}\n * Run: npx @pol-studios/powersync generate-schema\n *\n * DO NOT EDIT MANUALLY - changes will be overwritten\n */\n\nimport { column, Schema, Table } from \"@powersync/react-native\";\n`;\n}\n\n/**\n * Format indexes array for output\n */\nfunction formatIndexes(indexes: IndexDefinition[]): string {\n if (indexes.length === 0) return '';\n const indexLines = indexes.map(idx => {\n const columnsStr = idx.columns.map(c => `'${c}'`).join(', ');\n return ` { name: '${idx.name}', columns: [${columnsStr}] },`;\n });\n return `indexes: [\\n${indexLines.join('\\n')}\\n ]`;\n}\n\n/**\n * Generate the table definition for a parsed table\n */\nexport function generateTableDefinition(table: ParsedTable, columnDefs: string[], indexes: IndexDefinition[] = []): string {\n if (columnDefs.length === 0) {\n return `// ${table.name} - no syncable columns found`;\n }\n\n // Build options object\n const optionsParts: string[] = [];\n if (table.config.trackMetadata) {\n optionsParts.push('trackMetadata: true');\n }\n if (indexes.length > 0) {\n optionsParts.push(formatIndexes(indexes));\n }\n const optionsStr = optionsParts.length > 0 ? `, {\\n ${optionsParts.join(',\\n ')}\\n}` : '';\n return `const ${table.name} = new Table({\n${columnDefs.join('\\n')}\n}${optionsStr});`;\n}\n\n/**\n * Generate the schema export section\n */\nexport function generateSchemaExport(tableNames: string[]): string {\n return `// ============================================================================\n// SCHEMA EXPORT\n// ============================================================================\n\n// NOTE: photo_attachments is NOT included here.\n// The AttachmentQueue from @powersync/attachments creates and manages\n// its own internal SQLite table (not a view) during queue.init().\n// This allows INSERT/UPDATE operations to work correctly.\n\nexport const AppSchema = new Schema({\n${tableNames.map(name => ` ${name},`).join('\\n')}\n});\n\nexport type Database = (typeof AppSchema)[\"types\"];`;\n}\n\n/**\n * Generate schema mapping utilities\n */\nexport function generateSchemaMapping(tables: ParsedTable[], schemas: string[]): string {\n // Group tables by non-public schemas\n const schemaGroups = new Map<string, string[]>();\n for (const schema of schemas) {\n if (schema !== 'public') {\n schemaGroups.set(schema, []);\n }\n }\n for (const table of tables) {\n if (table.schema !== 'public' && schemaGroups.has(table.schema)) {\n schemaGroups.get(table.schema)!.push(table.name);\n }\n }\n const sections: string[] = [`// ============================================================================\n// SCHEMA MAPPING FOR CONNECTOR\n// ============================================================================`];\n\n // Generate constants for each non-public schema\n for (const [schema, tableNames] of schemaGroups) {\n if (tableNames.length > 0) {\n const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;\n sections.push(`\n// Tables in the '${schema}' schema (need .schema('${schema}') in Supabase queries)\nexport const ${constName} = new Set([\n${tableNames.map(name => ` \"${name}\",`).join('\\n')}\n]);`);\n }\n }\n\n // Generate helper function\n const schemaChecks = Array.from(schemaGroups.entries()).filter(([, names]) => names.length > 0).map(([schema]) => {\n const constName = `${schema.toUpperCase()}_SCHEMA_TABLES`;\n return ` if (${constName}.has(tableName)) return \"${schema}\";`;\n });\n if (schemaChecks.length > 0) {\n sections.push(`\n/**\n * Get the Supabase schema for a table\n */\nexport function getTableSchema(tableName: string): ${schemas.map(s => `\"${s}\"`).join(' | ')} {\n${schemaChecks.join('\\n')}\n return \"public\";\n}`);\n } else {\n sections.push(`\n/**\n * Get the Supabase schema for a table\n */\nexport function getTableSchema(tableName: string): \"public\" {\n return \"public\";\n}`);\n }\n return sections.join('\\n');\n}\n\n/**\n * Generate the FK detection utility (helpful for consumers)\n */\nexport function generateFKUtility(): string {\n return `\n// ============================================================================\n// FOREIGN KEY UTILITIES\n// ============================================================================\n\n/**\n * Check if a column name represents a foreign key reference\n * Convention: columns ending in 'Id' are foreign keys (e.g., projectId -> Project table)\n */\nexport function isForeignKeyColumn(columnName: string): boolean {\n return columnName.endsWith('Id') && columnName !== 'id';\n}\n\n/**\n * Get the referenced table name from a foreign key column\n * e.g., 'projectId' -> 'Project', 'equipmentFixtureUnitId' -> 'EquipmentFixtureUnit'\n */\nexport function getForeignKeyTable(columnName: string): string | null {\n if (!isForeignKeyColumn(columnName)) return null;\n // Remove 'Id' suffix and capitalize first letter\n const baseName = columnName.slice(0, -2);\n return baseName.charAt(0).toUpperCase() + baseName.slice(1);\n}`;\n}\n\n/**\n * Generate complete output file\n */\nexport function generateOutputFile(tables: ParsedTable[], tableDefs: string[], schemas: string[], typesPath: string): string {\n const tableNames = tables.map(t => t.name);\n return `${generateHeader(typesPath)}\n\n// ============================================================================\n// TABLE DEFINITIONS\n// ============================================================================\n\n${tableDefs.join('\\n\\n')}\n\n${generateSchemaExport(tableNames)}\n\n${generateSchemaMapping(tables, ['public', ...schemas.filter(s => s !== 'public')])}\n${generateFKUtility()}\n`;\n}","/**\n * Foreign Key Dependency Detection\n *\n * Analyzes parsed tables to detect FK relationships and determine\n * optimal upload order for offline-first sync.\n */\n\nimport type { ParsedTable } from './parser.js';\nexport interface FKDependency {\n /** Table containing the FK column */\n table: string;\n /** FK column name (e.g., 'projectId') */\n column: string;\n /** Referenced table name (e.g., 'Project') */\n referencedTable: string;\n}\nexport interface DependencyGraph {\n /** Map of table -> tables it depends on (has FK references to) */\n dependencies: Map<string, string[]>;\n /** Map of table -> tables that depend on it (have FKs referencing this table) */\n dependents: Map<string, string[]>;\n /** Topologically sorted table names for optimal upload order */\n uploadOrder: string[];\n /** All detected FK relationships */\n fkRelationships: FKDependency[];\n}\n\n/**\n * Convert a FK column name to its referenced table name\n * Convention: columns ending in 'Id' reference the PascalCase table\n * e.g., projectId -> Project, equipmentUnitId -> EquipmentUnit\n */\nexport function fkColumnToTableName(columnName: string): string | null {\n if (!columnName.endsWith('Id') || columnName === 'id') {\n return null;\n }\n // Remove 'Id' suffix and capitalize first letter\n const baseName = columnName.slice(0, -2);\n return baseName.charAt(0).toUpperCase() + baseName.slice(1);\n}\n\n/**\n * Detect FK dependencies from parsed tables\n *\n * Uses naming convention: columns ending in 'Id' reference the PascalCase table\n * Only considers references to tables that are in the provided list (ignores external references)\n */\nexport function detectFKDependencies(tables: ParsedTable[]): DependencyGraph {\n const tableNames = new Set(tables.map(t => t.name));\n const dependencies = new Map<string, string[]>();\n const dependents = new Map<string, string[]>();\n const fkRelationships: FKDependency[] = [];\n\n // Initialize maps for all tables\n for (const table of tables) {\n dependencies.set(table.name, []);\n dependents.set(table.name, []);\n }\n\n // Detect FK relationships\n for (const table of tables) {\n for (const [columnName] of table.columns) {\n const referencedTable = fkColumnToTableName(columnName);\n\n // Only track references to tables in our schema\n if (referencedTable && tableNames.has(referencedTable)) {\n // This table depends on the referenced table\n const tableDeps = dependencies.get(table.name) || [];\n if (!tableDeps.includes(referencedTable)) {\n tableDeps.push(referencedTable);\n dependencies.set(table.name, tableDeps);\n }\n\n // The referenced table has this table as a dependent\n const refDeps = dependents.get(referencedTable) || [];\n if (!refDeps.includes(table.name)) {\n refDeps.push(table.name);\n dependents.set(referencedTable, refDeps);\n }\n\n // Record the FK relationship\n fkRelationships.push({\n table: table.name,\n column: columnName,\n referencedTable\n });\n }\n }\n }\n\n // Calculate upload order via topological sort\n const uploadOrder = getUploadOrder({\n dependencies\n });\n return {\n dependencies,\n dependents,\n uploadOrder,\n fkRelationships\n };\n}\n\n/**\n * Get optimal upload order using Kahn's topological sort algorithm\n *\n * Tables with no dependencies are uploaded first, then tables that depend on them, etc.\n * This ensures that when a row references another table, the referenced row exists first.\n */\nexport function getUploadOrder(graph: Pick<DependencyGraph, 'dependencies'>): string[] {\n const result: string[] = [];\n const inDegree = new Map<string, number>();\n const adjList = new Map<string, string[]>();\n\n // Initialize in-degree (number of dependencies) for each table\n for (const [table, deps] of graph.dependencies) {\n inDegree.set(table, deps.length);\n adjList.set(table, []);\n }\n\n // Build adjacency list (reverse of dependencies - who depends on this table)\n for (const [table, deps] of graph.dependencies) {\n for (const dep of deps) {\n const list = adjList.get(dep) || [];\n list.push(table);\n adjList.set(dep, list);\n }\n }\n\n // Start with tables that have no dependencies\n const queue: string[] = [];\n for (const [table, degree] of inDegree) {\n if (degree === 0) {\n queue.push(table);\n }\n }\n\n // Sort queue alphabetically for deterministic output\n queue.sort();\n\n // Process queue\n while (queue.length > 0) {\n const table = queue.shift()!;\n result.push(table);\n\n // Reduce in-degree for all tables that depend on this one\n const dependentTables = adjList.get(table) || [];\n for (const dependent of dependentTables) {\n const newDegree = (inDegree.get(dependent) || 0) - 1;\n inDegree.set(dependent, newDegree);\n if (newDegree === 0) {\n queue.push(dependent);\n // Keep queue sorted for deterministic output\n queue.sort();\n }\n }\n }\n\n // Check for cycles (if result doesn't include all tables)\n if (result.length < graph.dependencies.size) {\n // There's a cycle - just append remaining tables alphabetically\n const remaining = [...graph.dependencies.keys()].filter(t => !result.includes(t)).sort();\n result.push(...remaining);\n }\n return result;\n}\n\n/**\n * Get tables that have no dependencies (root tables)\n * These are typically reference/lookup tables like User, Project, etc.\n */\nexport function getRootTables(graph: DependencyGraph): string[] {\n const roots: string[] = [];\n for (const [table, deps] of graph.dependencies) {\n if (deps.length === 0) {\n roots.push(table);\n }\n }\n return roots.sort();\n}\n\n/**\n * Get tables that have no dependents (leaf tables)\n * These are typically transaction/event tables that reference other tables\n */\nexport function getLeafTables(graph: DependencyGraph): string[] {\n const leaves: string[] = [];\n for (const [table, deps] of graph.dependents) {\n if (deps.length === 0) {\n leaves.push(table);\n }\n }\n return leaves.sort();\n}\n\n/**\n * Format the dependency graph as a human-readable string\n */\nexport function formatDependencyGraph(graph: DependencyGraph): string {\n const lines: string[] = [];\n lines.push('FK Dependencies:');\n for (const fk of graph.fkRelationships) {\n lines.push(` ${fk.table}.${fk.column} -> ${fk.referencedTable}`);\n }\n if (graph.fkRelationships.length === 0) {\n lines.push(' (none detected)');\n }\n lines.push('');\n lines.push('Upload Order (topological):');\n graph.uploadOrder.forEach((table, i) => {\n const deps = graph.dependencies.get(table) || [];\n const depStr = deps.length > 0 ? ` (depends on: ${deps.join(', ')})` : ' (root)';\n lines.push(` ${i + 1}. ${table}${depStr}`);\n });\n return lines.join('\\n');\n}"],"mappings":"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","names":[]}
|