@lunora/codegen 0.0.0 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +117 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +1473 -0
  5. package/dist/index.d.ts +1473 -0
  6. package/dist/index.mjs +28 -0
  7. package/dist/packem_shared/CONTAINERS_FILENAME-0K-pjNb8.mjs +224 -0
  8. package/dist/packem_shared/CodegenDiagnosticError-54jWDxA9.mjs +22 -0
  9. package/dist/packem_shared/LUNORA_ERROR_CODES-CySpQPD3.mjs +61 -0
  10. package/dist/packem_shared/buildOpenApiDocument-yHVN66Xd.mjs +183 -0
  11. package/dist/packem_shared/buildOpenRpcDocument-BZGY1-jT.mjs +60 -0
  12. package/dist/packem_shared/buildSchemaSnapshot-DzLDbWk3.mjs +233 -0
  13. package/dist/packem_shared/createCodegenProject-DGJm0_Pk.mjs +895 -0
  14. package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +13 -0
  15. package/dist/packem_shared/discoverAuthApiCalls-C35R6z0T.mjs +62 -0
  16. package/dist/packem_shared/discoverCrons-BL6iGuJ3.mjs +254 -0
  17. package/dist/packem_shared/discoverFunctions-DEgAcRuD.mjs +460 -0
  18. package/dist/packem_shared/discoverHttpRoutes-C978pBiG.mjs +131 -0
  19. package/dist/packem_shared/discoverInserts-CRQdXvHO.mjs +39 -0
  20. package/dist/packem_shared/discoverMaskProcedures-B64zA740.mjs +217 -0
  21. package/dist/packem_shared/discoverMigrations-Doj_-BAA.mjs +91 -0
  22. package/dist/packem_shared/discoverNondeterministicCalls-4KiPQxQU.mjs +122 -0
  23. package/dist/packem_shared/discoverQueries-BkIi0dBD.mjs +62 -0
  24. package/dist/packem_shared/discoverRlsMetadata-DpRB1HMe.mjs +280 -0
  25. package/dist/packem_shared/discoverSchema-BBulgGbH.mjs +542 -0
  26. package/dist/packem_shared/discoverStorageRulesMetadata-DAqJUxUv.mjs +97 -0
  27. package/dist/packem_shared/discoverWorkflows-DRDQdhfq.mjs +84 -0
  28. package/dist/packem_shared/emitApi-hRVC-kE7.mjs +2426 -0
  29. package/dist/packem_shared/emitApp-CzZ6GbrD.mjs +593 -0
  30. package/dist/packem_shared/lintSchema-DicbOHvH.mjs +68 -0
  31. package/dist/packem_shared/parse-validator-tuQtHrsr.mjs +132 -0
  32. package/dist/packem_shared/paths-BRd6JHuF.mjs +11 -0
  33. package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +57 -0
  34. package/package.json +45 -17
@@ -0,0 +1,233 @@
1
+ const SCHEMA_SNAPSHOT_VERSION = 1;
2
+ const encodeShardMode = (mode) => {
3
+ if (mode === "global" || mode === "root") {
4
+ return mode;
5
+ }
6
+ return `shardBy:${mode.field}`;
7
+ };
8
+ const fieldSnapshotOf = (validator) => {
9
+ if (validator.kind === "optional") {
10
+ return { kind: validator.inner?.kind ?? "unknown", optional: true };
11
+ }
12
+ return { kind: validator.kind, optional: false };
13
+ };
14
+ const tableSnapshotOf = (table) => {
15
+ const fields = {};
16
+ for (const [name, validator] of Object.entries(table.shape)) {
17
+ fields[name] = fieldSnapshotOf(validator);
18
+ }
19
+ const indexes = {};
20
+ for (const index of table.indexes) {
21
+ indexes[index.name] = { fields: [...index.fields], unique: index.unique ?? false };
22
+ }
23
+ const relations = {};
24
+ for (const relation of table.relations) {
25
+ relations[relation.name] = { field: relation.field, kind: relation.kind, table: relation.table };
26
+ }
27
+ return { fields, indexes, relations, shardMode: encodeShardMode(table.shardMode) };
28
+ };
29
+ const sortKeys = (record) => {
30
+ const sorted = {};
31
+ for (const key of Object.keys(record).toSorted((a, b) => a.localeCompare(b))) {
32
+ sorted[key] = record[key];
33
+ }
34
+ return sorted;
35
+ };
36
+ const buildSchemaSnapshot = (schema, migrationIds) => {
37
+ const tables = {};
38
+ for (const table of schema.tables) {
39
+ tables[table.name] = tableSnapshotOf(table);
40
+ }
41
+ return {
42
+ migrationIds: [...migrationIds].toSorted((a, b) => a.localeCompare(b)),
43
+ tables: sortKeys(tables),
44
+ version: SCHEMA_SNAPSHOT_VERSION
45
+ };
46
+ };
47
+ const serializeSchemaSnapshot = (snapshot) => `${JSON.stringify(snapshot, void 0, 2)}
48
+ `;
49
+ class SchemaSnapshotParseError extends Error {
50
+ name = "SchemaSnapshotParseError";
51
+ }
52
+ const isRecord = (value) => typeof value === "object" && value !== null;
53
+ const isValidTableSnapshot = (value) => isRecord(value) && isRecord(value.fields) && isRecord(value.indexes) && isRecord(value.relations) && typeof value.shardMode === "string";
54
+ const parseSchemaSnapshot = (content) => {
55
+ if (content === void 0 || content.trim() === "") {
56
+ return void 0;
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(content);
61
+ } catch (error) {
62
+ throw new SchemaSnapshotParseError(`baseline is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
63
+ }
64
+ if (!isRecord(parsed) || parsed.version !== SCHEMA_SNAPSHOT_VERSION || !isRecord(parsed.tables)) {
65
+ throw new SchemaSnapshotParseError(`baseline is malformed or written by an incompatible version (expected version ${String(SCHEMA_SNAPSHOT_VERSION)})`);
66
+ }
67
+ for (const [name, table] of Object.entries(parsed.tables)) {
68
+ if (!isValidTableSnapshot(table)) {
69
+ throw new SchemaSnapshotParseError(`baseline table "${name}" has an invalid structure`);
70
+ }
71
+ }
72
+ return {
73
+ migrationIds: Array.isArray(parsed.migrationIds) ? parsed.migrationIds : [],
74
+ tables: parsed.tables,
75
+ version: SCHEMA_SNAPSHOT_VERSION
76
+ };
77
+ };
78
+ const indexesEqual = (a, b) => a.unique === b.unique && a.fields.length === b.fields.length && a.fields.every((field, index) => field === b.fields[index]);
79
+ const diffExistingField = (tableName, name, old, field) => {
80
+ const changes = [];
81
+ if (old.kind !== field.kind) {
82
+ changes.push({
83
+ severity: "breaking",
84
+ summary: `field ${tableName}.${name} changed type: ${old.kind} → ${field.kind} — add a data migration to convert existing values`,
85
+ type: "changedFieldKind"
86
+ });
87
+ }
88
+ if (old.optional && !field.optional) {
89
+ changes.push({
90
+ severity: "breaking",
91
+ summary: `field ${tableName}.${name} became required — rows missing it would be invalid; add a data migration to backfill it`,
92
+ type: "fieldOptionalToRequired"
93
+ });
94
+ } else if (!old.optional && field.optional) {
95
+ changes.push({ severity: "safe", summary: `field ${tableName}.${name} became optional`, type: "fieldRequiredToOptional" });
96
+ }
97
+ return changes;
98
+ };
99
+ const addedFieldChange = (tableName, name, field) => field.optional ? { severity: "safe", summary: `added optional field ${tableName}.${name}`, type: "addedOptionalField" } : {
100
+ severity: "breaking",
101
+ summary: `added required field ${tableName}.${name} — existing rows have no value; add a data migration to backfill it`,
102
+ type: "addedRequiredField"
103
+ };
104
+ const diffFields = (tableName, baseline, current, changes) => {
105
+ for (const [name, field] of Object.entries(current.fields)) {
106
+ const old = baseline.fields[name];
107
+ if (old === void 0) {
108
+ changes.push(addedFieldChange(tableName, name, field));
109
+ } else {
110
+ changes.push(...diffExistingField(tableName, name, old, field));
111
+ }
112
+ }
113
+ for (const name of Object.keys(baseline.fields)) {
114
+ if (current.fields[name] === void 0) {
115
+ changes.push({
116
+ severity: "breaking",
117
+ summary: `removed field ${tableName}.${name} — add a data migration if stored data must be cleaned up`,
118
+ type: "removedField"
119
+ });
120
+ }
121
+ }
122
+ };
123
+ const diffIndexes = (tableName, baseline, current, changes) => {
124
+ for (const [name, index] of Object.entries(current.indexes)) {
125
+ const old = baseline.indexes[name];
126
+ if (old === void 0) {
127
+ changes.push({ severity: "safe", summary: `added index ${name} on ${tableName}`, type: "addedIndex" });
128
+ continue;
129
+ }
130
+ if (!indexesEqual(old, index)) {
131
+ changes.push({
132
+ severity: "breaking",
133
+ summary: `index ${name} on ${tableName} changed shape — a query may have relied on the old index`,
134
+ type: "changedIndex"
135
+ });
136
+ }
137
+ }
138
+ for (const name of Object.keys(baseline.indexes)) {
139
+ if (current.indexes[name] === void 0) {
140
+ changes.push({
141
+ severity: "breaking",
142
+ summary: `removed index ${name} on ${tableName} — a query that used \`.withIndex("${name}")\` would break`,
143
+ type: "removedIndex"
144
+ });
145
+ }
146
+ }
147
+ };
148
+ const diffRelations = (tableName, baseline, current, changes) => {
149
+ for (const name of Object.keys(current.relations)) {
150
+ if (baseline.relations[name] === void 0) {
151
+ changes.push({ severity: "safe", summary: `added relation ${tableName}.${name}`, type: "addedRelation" });
152
+ }
153
+ }
154
+ for (const name of Object.keys(baseline.relations)) {
155
+ if (current.relations[name] === void 0) {
156
+ changes.push({ severity: "breaking", summary: `removed relation ${tableName}.${name}`, type: "removedRelation" });
157
+ }
158
+ }
159
+ };
160
+ const diffExistingTable = (tableName, baseline, current, changes) => {
161
+ if (baseline.shardMode !== current.shardMode) {
162
+ changes.push({
163
+ severity: "breaking",
164
+ summary: `table ${tableName} changed shard mode: ${baseline.shardMode} → ${current.shardMode} — its physical storage moves; add a data migration / re-shard plan`,
165
+ type: "changedShardMode"
166
+ });
167
+ }
168
+ diffFields(tableName, baseline, current, changes);
169
+ diffIndexes(tableName, baseline, current, changes);
170
+ diffRelations(tableName, baseline, current, changes);
171
+ };
172
+ const diffSchemaSnapshots = (baseline, current) => {
173
+ const changes = [];
174
+ const baselineTables = baseline?.tables ?? {};
175
+ for (const [tableName, table] of Object.entries(current.tables)) {
176
+ const old = baselineTables[tableName];
177
+ if (old === void 0) {
178
+ changes.push({ severity: "safe", summary: `added table ${tableName}`, type: "addedTable" });
179
+ continue;
180
+ }
181
+ diffExistingTable(tableName, old, table, changes);
182
+ }
183
+ for (const tableName of Object.keys(baselineTables)) {
184
+ if (current.tables[tableName] === void 0) {
185
+ changes.push({
186
+ severity: "breaking",
187
+ summary: `removed table ${tableName} — add a data migration if its data must be archived/cleaned up`,
188
+ type: "removedTable"
189
+ });
190
+ }
191
+ }
192
+ return { changes };
193
+ };
194
+ const evaluateSchemaDrift = (options) => {
195
+ const { allowDrift = false, baseline, current } = options;
196
+ const drift = diffSchemaSnapshots(baseline, current);
197
+ const breaking = drift.changes.filter((change) => change.severity === "breaking");
198
+ const newMigrationIds = current.migrationIds.filter((id) => !(baseline?.migrationIds ?? []).includes(id));
199
+ if (drift.changes.length === 0 || baseline === void 0) {
200
+ return { blocked: false, changes: drift.changes, newMigrationIds, reason: "" };
201
+ }
202
+ if (breaking.length === 0) {
203
+ const summary = drift.changes.map((change) => ` - ${change.summary}`).join("\n");
204
+ return { blocked: false, changes: drift.changes, newMigrationIds, reason: `schema drift detected (all additive/safe):
205
+ ${summary}` };
206
+ }
207
+ const breakingSummary = breaking.map((change) => ` - ${change.summary}`).join("\n");
208
+ if (newMigrationIds.length > 0) {
209
+ return {
210
+ blocked: false,
211
+ changes: drift.changes,
212
+ newMigrationIds,
213
+ reason: `breaking schema drift detected, but ${String(newMigrationIds.length)} new migration(s) were added (${newMigrationIds.join(", ")}):
214
+ ${breakingSummary}`
215
+ };
216
+ }
217
+ const reason = `deploy blocked: ${String(breaking.length)} breaking schema change(s) detected since the last blessed schema baseline — these changes are incompatible with existing data without a migration:
218
+ ${breakingSummary}
219
+
220
+ To fix:
221
+ • For breaking changes: add a \`defineMigration({ id, table, up })\` in lunora/ for each affected table, then run \`lunora migrate\` (or \`lunora codegen\`) to apply and re-bless the baseline.
222
+ • For backward-compatible changes (e.g. adding an optional field): pass \`--allow-schema-drift\` to skip the block.
223
+ • To accept the new shape without a migration (you know data is compatible): pass \`--update-schema-baseline\`.
224
+ Docs: https://lunora.dev/docs/migrations`;
225
+ if (allowDrift) {
226
+ return { blocked: false, changes: drift.changes, newMigrationIds, reason: `${reason}
227
+
228
+ (overridden by --allow-schema-drift)` };
229
+ }
230
+ return { blocked: true, changes: drift.changes, newMigrationIds, reason };
231
+ };
232
+
233
+ export { SCHEMA_SNAPSHOT_VERSION, SchemaSnapshotParseError, buildSchemaSnapshot, diffSchemaSnapshots, evaluateSchemaDrift, parseSchemaSnapshot, serializeSchemaSnapshot };