@everystack/cli 0.4.100 → 0.4.102

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 (46) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/package.json +3 -3
  3. package/src/cli/authz-baseline.ts +8 -4
  4. package/src/cli/authz-compile.ts +61 -7
  5. package/src/cli/authz-contract.ts +23 -14
  6. package/src/cli/authz-derive.ts +282 -30
  7. package/src/cli/authz-reconcile.ts +6 -4
  8. package/src/cli/commands/db-check.ts +40 -0
  9. package/src/cli/commands/db-generate.ts +10 -9
  10. package/src/cli/commands/db-plan.ts +30 -13
  11. package/src/cli/commands/db-pull.ts +153 -63
  12. package/src/cli/commands/db-reconcile.ts +22 -3
  13. package/src/cli/commands/db-sync.ts +6 -5
  14. package/src/cli/commands/db.ts +91 -8
  15. package/src/cli/commands/update.ts +3 -2
  16. package/src/cli/db-build.ts +1 -0
  17. package/src/cli/declared-derived.ts +3 -1
  18. package/src/cli/declared-diff.ts +4 -1
  19. package/src/cli/derived-apply.ts +116 -10
  20. package/src/cli/derived-grants.ts +4 -2
  21. package/src/cli/derived-introspect.ts +23 -3
  22. package/src/cli/derived-plan.ts +37 -1
  23. package/src/cli/derived-render.ts +33 -34
  24. package/src/cli/edge-plan.ts +57 -9
  25. package/src/cli/generated-artifacts.ts +22 -0
  26. package/src/cli/generated-names.ts +108 -0
  27. package/src/cli/host-url.ts +48 -27
  28. package/src/cli/index.ts +5 -5
  29. package/src/cli/lifecycle-canon.ts +37 -0
  30. package/src/cli/lifecycle-derived.ts +721 -0
  31. package/src/cli/lifecycle-pull.ts +177 -0
  32. package/src/cli/lifecycle-verify.ts +232 -0
  33. package/src/cli/migration-compile.ts +8 -3
  34. package/src/cli/migration-generate.ts +23 -3
  35. package/src/cli/model-map.ts +255 -0
  36. package/src/cli/model-render.ts +226 -76
  37. package/src/cli/plain-output.ts +198 -10
  38. package/src/cli/runbook/model-section.ts +8 -5
  39. package/src/cli/schema-compile.ts +35 -17
  40. package/src/cli/schema-diff.ts +166 -31
  41. package/src/cli/schema-fingerprint.ts +39 -6
  42. package/src/cli/schema-introspect.ts +64 -2
  43. package/src/cli/schema-source.ts +40 -9
  44. package/src/cli/schema-swap.ts +3 -2
  45. package/src/cli/state-apply.ts +10 -4
  46. package/src/cli/ts-template.ts +52 -0
@@ -30,7 +30,7 @@
30
30
  * so the swap is a pointer flip, never a recompute.
31
31
  */
32
32
 
33
- import type { ModelDescriptor } from '@everystack/model';
33
+ import { relationDeleteActions, type ModelDescriptor } from '@everystack/model';
34
34
  import { modelForeignKeys, qualifiedTable } from './schema-compile';
35
35
  import { compileTableContract } from './authz-compile';
36
36
  import { emitSwapAuthzSql } from './authz-reconcile';
@@ -73,10 +73,11 @@ export interface CrossSchemaFk {
73
73
  */
74
74
  export function crossSchemaForeignKeys(models: ModelDescriptor[], targetSchema: string): CrossSchemaFk[] {
75
75
  const out: CrossSchemaFk[] = [];
76
+ const dependents = relationDeleteActions(models);
76
77
  for (const model of models) {
77
78
  if ((model.schema || 'public') === targetSchema) continue; // internal edge, not external
78
79
  const from = qualifiedTable(model);
79
- for (const fk of modelForeignKeys(model)) {
80
+ for (const fk of modelForeignKeys(model, dependents)) {
80
81
  if (schemaOf(fk.refTable) !== targetSchema) continue; // not pointing into the swapped schema
81
82
  const cols = fk.columns.map((c) => `"${c}"`).join(', ');
82
83
  const refCols = fk.refColumns.map((c) => `"${c}"`).join(', ');
@@ -20,7 +20,7 @@
20
20
  import { execSync, execFileSync } from 'node:child_process';
21
21
  import type { ModelDescriptor } from '@everystack/model';
22
22
  import { introspectContract, type AuthzContract, type QueryRunner } from './authz-contract';
23
- import { borrowedSessionRunner, type SessionRunner } from './session';
23
+ import { borrowedSessionRunner, CANONICAL_SEARCH_PATH, type SessionRunner } from './session';
24
24
  import { introspectSchema, type SchemaSnapshot } from './schema-introspect';
25
25
  import { FUNCTIONS_SQL, contractFunctionRow } from './security-catalog';
26
26
  import { generateMigrationSql, HELD_DROP_PREFIX } from './migration-generate';
@@ -572,9 +572,14 @@ export async function applyGeneratedStatements(
572
572
  // point; they join the row on the amendment, where their outcome is known.
573
573
  const prerequisiteSql = prerequisites
574
574
  .flatMap((prerequisite) => [renderStatePrerequisiteGuard(prerequisite), prerequisite.sql]);
575
+ // The plan is rendered against the canonical path — public bare, non-public qualified — so it
576
+ // must RUN under it. Under an operator path like `analytics, public`, a bare
577
+ // `CREATE TYPE "post_status"` would land in analytics. Transaction-local and inside the
578
+ // batch's own string, so it can neither be skipped nor outlive the batch.
579
+ const stateBatch = batch.length > 0 ? [renderLocalSearchPath(CANONICAL_SEARCH_PATH), ...batch] : [];
575
580
  // A SQL-language prerequisite may read a table this same state edge creates. PostgreSQL
576
581
  // validates such bodies at CREATE time unless this transaction-local setting defers it.
577
- let batchSql = [...(prerequisites.length > 0 ? ['SET LOCAL check_function_bodies = off'] : []), ...prerequisiteSql, ...batch].join('\n;\n');
582
+ let batchSql = [...(prerequisites.length > 0 ? ['SET LOCAL check_function_bodies = off'] : []), ...prerequisiteSql, ...stateBatch].join('\n;\n');
578
583
  const validationSql = validations.join('\n;\n');
579
584
  const started = now();
580
585
  try {
@@ -587,13 +592,14 @@ export async function applyGeneratedStatements(
587
592
  const originalPath = settings?.search_path;
588
593
  if (typeof originalPath !== 'string') throw new Error('could not capture prerequisite search_path; no state was applied.');
589
594
  // Defaults and SQL-standard bodies bind names at CREATE time. Use the full declared
590
- // path recorded at mint, then restore the exact connection setting before state DDL.
595
+ // path recorded at mint, then restore the exact connection setting; the state DDL
596
+ // that follows pins the canonical path itself.
591
597
  batchSql = ['SET LOCAL check_function_bodies = off', ...prerequisites.flatMap((prerequisite) => [
592
598
  renderStatePrerequisiteGuard(prerequisite),
593
599
  ...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(prerequisite.searchPath)]),
594
600
  prerequisite.sql,
595
601
  ...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(originalPath)]),
596
- ]), ...batch].join('\n;\n');
602
+ ]), ...stateBatch].join('\n;\n');
597
603
  await transactionRunner(batchSql);
598
604
  // `defHash` is PostgreSQL's canonical deparse, so source text cannot predict it. Read it
599
605
  // after the CREATE through the OPEN transaction, then write provenance before COMMIT.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Escape live text for the body of a template literal in generated source, so the template
3
+ * evaluates back to EXACTLY that text: backslash first, then backtick, then `${`. Live database
4
+ * text is data. Unescaped, `${…}` in it ran when the generated file was imported, and a
5
+ * backslash before a backtick closed the template. Every renderer that puts live text inside
6
+ * sql`…` uses this one rule.
7
+ */
8
+ export function templateBody(text: string): string {
9
+ return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
10
+ }
11
+
12
+ /**
13
+ * Live text as a single-quoted string literal in generated source, evaluating back to EXACTLY
14
+ * that text. Backslash first, then the quote, then every line terminator: a raw newline or
15
+ * carriage return ends a string literal, so a live name holding one broke the generated file.
16
+ * U+2028 and U+2029 are legal inside a string since ES2019 but not in every older parser, so
17
+ * they are escaped too. Ordinary text comes out unchanged.
18
+ */
19
+ export function tsString(text: string): string {
20
+ const body = text
21
+ .replace(/\\/g, '\\\\')
22
+ .replace(/'/g, "\\'")
23
+ .replace(/\n/g, '\\n')
24
+ .replace(/\r/g, '\\r')
25
+ .replace(/\u2028/g, '\\u2028')
26
+ .replace(/\u2029/g, '\\u2029');
27
+ return `'${body}'`;
28
+ }
29
+
30
+ /**
31
+ * Live text bound for a `//` line comment in generated source. JavaScript ends a line comment
32
+ * at ANY line terminator (LF, CR, U+2028, U+2029), so a live name holding one ended the comment
33
+ * and the rest of the name ran as code. Each terminator becomes its visible escape sequence
34
+ * (`\n`, `\r`, `\u2028`, `\u2029`): the comment stays one line, and a reader still sees that
35
+ * the live name held a line break. Ordinary text comes out unchanged.
36
+ */
37
+ export function commentText(text: string): string {
38
+ return text
39
+ .replace(/\n/g, '\\n')
40
+ .replace(/\r/g, '\\r')
41
+ .replace(/\u2028/g, '\\u2028')
42
+ .replace(/\u2029/g, '\\u2029');
43
+ }
44
+
45
+ /**
46
+ * Live text that is MEANT to span several comment lines (a deparsed predicate in a note): split
47
+ * on every line terminator, so each piece gets its own `//`. Splitting on `\n` alone left a CR,
48
+ * U+2028 or U+2029 inside one line, which still ended the comment.
49
+ */
50
+ export function commentLines(text: string): string[] {
51
+ return text.split(/\r\n|[\n\r\u2028\u2029]/);
52
+ }