@everystack/cli 0.3.28 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +6 -2
- package/src/cli/apply-execute.ts +185 -0
- package/src/cli/authz-compile.ts +5 -2
- package/src/cli/authz-lint.ts +48 -0
- package/src/cli/aws.ts +102 -4
- package/src/cli/backfill.ts +1 -1
- package/src/cli/commands/db-apply.ts +159 -169
- package/src/cli/commands/db-backfill.ts +2 -2
- package/src/cli/commands/db-backup.ts +34 -0
- package/src/cli/commands/db-branch.ts +28 -4
- package/src/cli/commands/db-check.ts +97 -34
- package/src/cli/commands/db-fingerprint.ts +9 -4
- package/src/cli/commands/db-generate.ts +73 -22
- package/src/cli/commands/db-plan.ts +7 -13
- package/src/cli/commands/db-pull.ts +39 -7
- package/src/cli/commands/db-reconcile.ts +219 -66
- package/src/cli/commands/db-sync.ts +58 -21
- package/src/cli/commands/deploy.ts +4 -0
- package/src/cli/commands/pipeline-run.ts +269 -0
- package/src/cli/db-build.ts +9 -3
- package/src/cli/db-source.ts +83 -7
- package/src/cli/declared-derived.ts +136 -0
- package/src/cli/derived-apply.ts +40 -6
- package/src/cli/derived-compile.ts +327 -0
- package/src/cli/derived-grants.ts +96 -0
- package/src/cli/derived-introspect.ts +258 -21
- package/src/cli/derived-lint.ts +261 -0
- package/src/cli/derived-plan.ts +194 -12
- package/src/cli/derived-render.ts +320 -0
- package/src/cli/derived-source.ts +53 -125
- package/src/cli/discover.ts +16 -0
- package/src/cli/git-descent.ts +15 -2
- package/src/cli/index.ts +26 -6
- package/src/cli/migration-compile.ts +38 -7
- package/src/cli/migration-generate.ts +43 -4
- package/src/cli/model-render.ts +106 -13
- package/src/cli/models-path.ts +1 -1
- package/src/cli/ops-diagnostics.ts +71 -0
- package/src/cli/pipeline-loader.ts +68 -0
- package/src/cli/pipeline-path.ts +25 -0
- package/src/cli/schema-compile.ts +41 -10
- package/src/cli/schema-diff.ts +123 -17
- package/src/cli/schema-fingerprint.ts +28 -18
- package/src/cli/schema-introspect.ts +175 -8
- package/src/cli/schema-source.ts +75 -6
- package/src/cli/state-apply.ts +4 -1
package/src/cli/schema-source.ts
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* Input is a `@everystack/model` ModelDescriptor (type-only — no runtime dep).
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import type { ModelDescriptor, FieldSpec } from '@everystack/model';
|
|
28
|
+
import type { ModelDescriptor, FieldSpec, DerivedDescriptor, ViewDescriptor, MaterializedViewDescriptor } from '@everystack/model';
|
|
29
|
+
import { parseQualified } from './derived-source.js';
|
|
29
30
|
|
|
30
31
|
/** `author_id` is the SQL identifier; `authorId` the field key. The single snake-case rule. */
|
|
31
32
|
function toSnakeCase(name: string): string {
|
|
@@ -144,6 +145,7 @@ function columnModifiers(
|
|
|
144
145
|
if (spec.defaultKind === 'now') s += '.defaultNow()';
|
|
145
146
|
else if (spec.defaultKind === 'random') s += '.defaultRandom()';
|
|
146
147
|
else if (spec.defaultKind === 'value') s += `.default(${defaultValueLiteral(spec.default)})`;
|
|
148
|
+
else if (spec.defaultKind === 'sql') s += `.default(sql.raw(${strLiteral(String(spec.default))}))`;
|
|
147
149
|
|
|
148
150
|
if (spec.isPrimaryKey && !isComposite) s += '.primaryKey()';
|
|
149
151
|
|
|
@@ -250,6 +252,25 @@ function relationNameFragment(
|
|
|
250
252
|
export interface CompileDrizzleSourceOptions {
|
|
251
253
|
/** Reserved for parity with the DDL compiler's `schema` option. Unused in the emit. */
|
|
252
254
|
schema?: string;
|
|
255
|
+
/**
|
|
256
|
+
* The declared derived layer (B6a): views/matviews that declare `fields` are emitted
|
|
257
|
+
* as typed `pgView(...)`/`pgMaterializedView(...)` `.existing()` blocks — the
|
|
258
|
+
* reconciler owns their DDL; the generated schema carries only the read shape.
|
|
259
|
+
* Relations without `fields` and non-relation kinds are skipped.
|
|
260
|
+
*/
|
|
261
|
+
derived?: DerivedDescriptor[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The derived relations that opted into typed emission, identity-sorted (deterministic). */
|
|
265
|
+
function typedRelations(derived: DerivedDescriptor[]): (ViewDescriptor | MaterializedViewDescriptor)[] {
|
|
266
|
+
return derived
|
|
267
|
+
.filter((d): d is ViewDescriptor | MaterializedViewDescriptor =>
|
|
268
|
+
(d.kind === 'view' || d.kind === 'materialized view') && !!d.fields && Object.keys(d.fields).length > 0)
|
|
269
|
+
.sort((a, b) => {
|
|
270
|
+
const qa = parseQualified(a.name);
|
|
271
|
+
const qb = parseQualified(b.name);
|
|
272
|
+
return qa.schema.localeCompare(qb.schema) || qa.name.localeCompare(qb.name);
|
|
273
|
+
});
|
|
253
274
|
}
|
|
254
275
|
|
|
255
276
|
/**
|
|
@@ -264,14 +285,15 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
264
285
|
// generated schema. (The migration generator still emits its SQL + authz.) The
|
|
265
286
|
// command and the drift guard both call this, so filtering here keeps them in sync.
|
|
266
287
|
const models = allModels.filter((m) => !m.private);
|
|
288
|
+
const derivedRelations = typedRelations(_opts.derived ?? []);
|
|
267
289
|
|
|
268
290
|
const modelsByDescriptor = new Map<ModelDescriptor, { camel: string }>();
|
|
269
291
|
for (const model of models) modelsByDescriptor.set(model, { camel: toCamelCase(model.table) });
|
|
270
292
|
|
|
271
293
|
// --- Collect enums (deduped by name, sorted) -----------------------------
|
|
272
294
|
const enumsByName = new Map<string, readonly string[]>();
|
|
273
|
-
|
|
274
|
-
for (const f of Object.values(
|
|
295
|
+
const collectEnums = (fields: Record<string, { spec: FieldSpec }>) => {
|
|
296
|
+
for (const f of Object.values(fields)) {
|
|
275
297
|
const { type, enumName, enumValues } = f.spec;
|
|
276
298
|
if (type !== 'enum' || !enumName) continue;
|
|
277
299
|
const values = enumValues ?? [];
|
|
@@ -281,7 +303,9 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
281
303
|
}
|
|
282
304
|
if (!existing) enumsByName.set(enumName, values);
|
|
283
305
|
}
|
|
284
|
-
}
|
|
306
|
+
};
|
|
307
|
+
for (const model of models) collectEnums(model.fields);
|
|
308
|
+
for (const rel of derivedRelations) collectEnums(rel.fields!);
|
|
285
309
|
const enumNames = [...enumsByName.keys()].sort((a, b) => a.localeCompare(b));
|
|
286
310
|
|
|
287
311
|
// --- Track which pg-core builders + drizzle-orm symbols are used ----------
|
|
@@ -303,6 +327,7 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
303
327
|
const spec = builder.spec;
|
|
304
328
|
const { call, builder: pgBuilder } = baseColumnSource(toSnakeCase(key), spec);
|
|
305
329
|
pgCoreBuilders.add(pgBuilder);
|
|
330
|
+
if (spec.defaultKind === 'sql') pgCoreBuilders.add('sql'); // imported from 'drizzle-orm', handled below
|
|
306
331
|
const mods = columnModifiers(spec, isComposite, modelsByDescriptor);
|
|
307
332
|
if (spec.isDeprecated) {
|
|
308
333
|
// The contract phase (decision 13): the column stays in the database and
|
|
@@ -381,14 +406,57 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
381
406
|
);
|
|
382
407
|
}
|
|
383
408
|
|
|
409
|
+
// --- Emit derived relation blocks (typed reads — B6a) ----------------------
|
|
410
|
+
// Views/matviews that declared `fields` become `.existing()` declarations: the
|
|
411
|
+
// reconciler owns their DDL, so drizzle never creates them — the block carries
|
|
412
|
+
// ONLY the read shape (type, nullability, array). Identity-sorted for the
|
|
413
|
+
// drift-guard byte-match; column names snake_case by the same single rule.
|
|
414
|
+
const derivedBlocks: string[] = [];
|
|
415
|
+
for (const rel of derivedRelations) {
|
|
416
|
+
const { schema, name } = parseQualified(rel.name);
|
|
417
|
+
const camel = toCamelCase(schema === 'public' ? name : `${schema}_${name}`);
|
|
418
|
+
|
|
419
|
+
const colLines: string[] = [];
|
|
420
|
+
for (const [key, builder] of Object.entries(rel.fields!)) {
|
|
421
|
+
const spec = builder.spec;
|
|
422
|
+
const { call, builder: pgBuilder } = baseColumnSource(toSnakeCase(key), spec);
|
|
423
|
+
pgCoreBuilders.add(pgBuilder);
|
|
424
|
+
// Only .array()/.notNull() can appear — defineView/defineMaterializedView
|
|
425
|
+
// reject every table-structural modifier at define time.
|
|
426
|
+
const mods = columnModifiers(spec, true, modelsByDescriptor);
|
|
427
|
+
colLines.push(` ${key}: ${call}${mods},`);
|
|
428
|
+
}
|
|
429
|
+
const cols = `{\n${colLines.join('\n')}\n}`;
|
|
430
|
+
|
|
431
|
+
if (schema === 'public') {
|
|
432
|
+
const topFn = rel.kind === 'view' ? 'pgView' : 'pgMaterializedView';
|
|
433
|
+
pgCoreBuilders.add(topFn);
|
|
434
|
+
derivedBlocks.push(`export const ${camel} = ${topFn}(${strLiteral(name)}, ${cols}).existing();`);
|
|
435
|
+
} else {
|
|
436
|
+
const schemaFn = rel.kind === 'view' ? 'view' : 'materializedView';
|
|
437
|
+
pgCoreBuilders.add('pgSchema');
|
|
438
|
+
derivedBlocks.push(`export const ${camel} = pgSchema(${strLiteral(schema)}).${schemaFn}(${strLiteral(name)}, ${cols}).existing();`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
384
442
|
// --- Header ----------------------------------------------------------------
|
|
385
|
-
const
|
|
443
|
+
const headerLines = [
|
|
386
444
|
'// GENERATED by `everystack db:generate` from models/. Do not edit.',
|
|
387
445
|
'//',
|
|
388
446
|
'// Cross-schema foreign keys (e.g. profiles.id -> auth.users.id) are intentionally',
|
|
389
447
|
'// omitted: a Model cannot reference a table outside the model set, so the runtime',
|
|
390
448
|
'// table and its relations() never carry that FK. The DB constraint stays untouched.',
|
|
391
|
-
]
|
|
449
|
+
];
|
|
450
|
+
if (derivedBlocks.length) {
|
|
451
|
+
headerLines.push(
|
|
452
|
+
'//',
|
|
453
|
+
'// Derived relations (views/matviews) are declared `.existing()`: db:reconcile owns',
|
|
454
|
+
'// their DDL — these blocks carry only the read shape their descriptors declare.',
|
|
455
|
+
'// That shape is author-declared, NOT introspected: a wrong field type or',
|
|
456
|
+
'// nullability in the descriptor mistypes reads silently. Fix the descriptor.',
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const header = headerLines.join('\n');
|
|
392
460
|
|
|
393
461
|
// --- Imports ---------------------------------------------------------------
|
|
394
462
|
// pg-core: sorted, enum consts are not imports (they're declared below), `sql`
|
|
@@ -422,6 +490,7 @@ export function compileDrizzleSource(allModels: ModelDescriptor[], _opts: Compil
|
|
|
422
490
|
if (enumDecls.length) sections.push(enumDecls.join('\n'));
|
|
423
491
|
for (const block of tableBlocks) sections.push(block);
|
|
424
492
|
for (const block of relationBlocks) sections.push(block);
|
|
493
|
+
for (const block of derivedBlocks) sections.push(block);
|
|
425
494
|
|
|
426
495
|
return sections.join('\n\n') + '\n';
|
|
427
496
|
}
|
package/src/cli/state-apply.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { introspectContract, type AuthzContract, type QueryRunner } from './auth
|
|
|
23
23
|
import { introspectSchema, type SchemaSnapshot } from './schema-introspect.js';
|
|
24
24
|
import { FUNCTIONS_SQL, contractFunctionRow } from './security-catalog.js';
|
|
25
25
|
import { generateMigrationSql, HELD_DROP_PREFIX } from './migration-generate.js';
|
|
26
|
+
import type { SequenceDescriptor } from '@everystack/model';
|
|
26
27
|
import { fingerprintLive } from './schema-fingerprint.js';
|
|
27
28
|
import { ENSURE_RECONCILER_SQL, renderSchemaLogInsert, renderSchemaLogFingerprintUpdate } from './derived-apply.js';
|
|
28
29
|
|
|
@@ -170,6 +171,8 @@ export async function applyGeneratedStatements(
|
|
|
170
171
|
export interface StateSyncOptions extends StateApplyOptions {
|
|
171
172
|
/** Passed through to the verifying re-diff so held-vs-real matches the plan. */
|
|
172
173
|
allowDrops?: boolean;
|
|
174
|
+
/** Standalone sequences — the verify re-diff must see the same declared state as the plan. */
|
|
175
|
+
sequences?: SequenceDescriptor[];
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
export interface StateSyncOutcome {
|
|
@@ -228,7 +231,7 @@ export async function applyStateAndVerify(
|
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
const remaining = classifyGeneratedStatements(
|
|
231
|
-
generateMigrationSql(models, snapshot, { allowDrops: options.allowDrops, liveAuthz: contract }),
|
|
234
|
+
generateMigrationSql(models, snapshot, { allowDrops: options.allowDrops, liveAuthz: contract, sequences: options.sequences }),
|
|
232
235
|
).executable;
|
|
233
236
|
|
|
234
237
|
return {
|