@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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/package.json +6 -2
  3. package/src/cli/apply-execute.ts +185 -0
  4. package/src/cli/authz-compile.ts +5 -2
  5. package/src/cli/authz-lint.ts +48 -0
  6. package/src/cli/aws.ts +102 -4
  7. package/src/cli/backfill.ts +1 -1
  8. package/src/cli/commands/db-apply.ts +159 -169
  9. package/src/cli/commands/db-backfill.ts +2 -2
  10. package/src/cli/commands/db-backup.ts +34 -0
  11. package/src/cli/commands/db-branch.ts +28 -4
  12. package/src/cli/commands/db-check.ts +97 -34
  13. package/src/cli/commands/db-fingerprint.ts +9 -4
  14. package/src/cli/commands/db-generate.ts +73 -22
  15. package/src/cli/commands/db-plan.ts +7 -13
  16. package/src/cli/commands/db-pull.ts +39 -7
  17. package/src/cli/commands/db-reconcile.ts +219 -66
  18. package/src/cli/commands/db-sync.ts +58 -21
  19. package/src/cli/commands/deploy.ts +4 -0
  20. package/src/cli/commands/pipeline-run.ts +269 -0
  21. package/src/cli/db-build.ts +9 -3
  22. package/src/cli/db-source.ts +83 -7
  23. package/src/cli/declared-derived.ts +136 -0
  24. package/src/cli/derived-apply.ts +40 -6
  25. package/src/cli/derived-compile.ts +327 -0
  26. package/src/cli/derived-grants.ts +96 -0
  27. package/src/cli/derived-introspect.ts +258 -21
  28. package/src/cli/derived-lint.ts +261 -0
  29. package/src/cli/derived-plan.ts +194 -12
  30. package/src/cli/derived-render.ts +320 -0
  31. package/src/cli/derived-source.ts +53 -125
  32. package/src/cli/discover.ts +16 -0
  33. package/src/cli/git-descent.ts +15 -2
  34. package/src/cli/index.ts +26 -6
  35. package/src/cli/migration-compile.ts +38 -7
  36. package/src/cli/migration-generate.ts +43 -4
  37. package/src/cli/model-render.ts +106 -13
  38. package/src/cli/models-path.ts +1 -1
  39. package/src/cli/ops-diagnostics.ts +71 -0
  40. package/src/cli/pipeline-loader.ts +68 -0
  41. package/src/cli/pipeline-path.ts +25 -0
  42. package/src/cli/schema-compile.ts +41 -10
  43. package/src/cli/schema-diff.ts +123 -17
  44. package/src/cli/schema-fingerprint.ts +28 -18
  45. package/src/cli/schema-introspect.ts +175 -8
  46. package/src/cli/schema-source.ts +75 -6
  47. package/src/cli/state-apply.ts +4 -1
@@ -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
- for (const model of models) {
274
- for (const f of Object.values(model.fields)) {
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 header = [
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
- ].join('\n');
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
  }
@@ -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 {