@everystack/cli 0.3.14 → 0.3.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -44,6 +44,7 @@ import { writeContract, loadContract } from '../authz-contract-io.js';
44
44
  import { renderContractMarkdown } from '../authz-render.js';
45
45
  import { FUNCTIONS_SQL, catalogFunctionToDescriptor } from '../security-catalog.js';
46
46
  import { resolveConfig, opsFunction } from '../config.js';
47
+ import { resolveModelsPath } from '../models-path.js';
47
48
  import { invokeAction } from '../aws.js';
48
49
  import { step, success, fail, info, warn } from '../output.js';
49
50
 
@@ -239,7 +240,7 @@ async function loadModels(modelsPath: string): Promise<ModelDescriptor[]> {
239
240
  * exit on any leak or vacuous policy; an `unprobed` table (one identity) is a warning, not a fail.
240
241
  */
241
242
  export async function dbAuthzOwnerCommand(flags: Record<string, string>): Promise<void> {
242
- const modelsPath = flags.models || 'models/index.ts';
243
+ const modelsPath = resolveModelsPath(flags.models);
243
244
  const schema = flags.schema || 'public';
244
245
 
245
246
  let probes: OwnerProbe[];
@@ -28,11 +28,11 @@ import { generateMigrationSql, unmodeledTables, formatMigrationFile, planMigrati
28
28
  import { introspectContract, type QueryRunner } from '../authz-contract.js';
29
29
  import { FUNCTIONS_SQL, catalogFunctionToDescriptor } from '../security-catalog.js';
30
30
  import { resolveDbSource, createUrlRunner, type DbSource } from '../db-source.js';
31
+ import { resolveModelsPath } from '../models-path.js';
31
32
  import { resolveConfig, opsFunction } from '../config.js';
32
33
  import { invokeAction } from '../aws.js';
33
34
  import { step, success, fail, info, warn } from '../output.js';
34
35
 
35
- const DEFAULT_MODELS = 'models/index.ts';
36
36
  const DEFAULT_MIGRATIONS = 'drizzle';
37
37
  const DEFAULT_SCHEMA_OUT = 'db/schema.generated.ts';
38
38
 
@@ -123,7 +123,7 @@ async function dbGenerateInit(modelsPath: string, migrationsDir: string, schemaO
123
123
  }
124
124
 
125
125
  export async function dbGenerateCommand(flags: Record<string, string>): Promise<void> {
126
- const modelsPath = flags.models || DEFAULT_MODELS;
126
+ const modelsPath = resolveModelsPath(flags.models);
127
127
  const migrationsDir = path.resolve(flags.dir || DEFAULT_MIGRATIONS);
128
128
  const schemaOut = path.resolve(flags['schema-out'] || DEFAULT_SCHEMA_OUT);
129
129
  const name = flags.name || 'generated';
@@ -13,7 +13,7 @@
13
13
  * the reconciler writes it and never reads it.
14
14
  */
15
15
 
16
- import type { SourceObject } from './derived-source.js';
16
+ import { normalizeSql, type SourceObject } from './derived-source.js';
17
17
  import type { ReconcilePlan } from './derived-plan.js';
18
18
  import { quoteQualified } from './pg-ident.js';
19
19
 
@@ -69,9 +69,22 @@ export function ensureOrReplace(sql: string): string {
69
69
  return sql.replace(/^(\s*)CREATE\s+FUNCTION/i, '$1CREATE OR REPLACE FUNCTION');
70
70
  }
71
71
 
72
- /** An object's full creation SQL: the CREATE statement plus its attachments, in source order. */
72
+ const WITH_NO_DATA_RE = /WITH\s+NO\s+DATA\s*$/i;
73
+
74
+ /**
75
+ * An object's full creation SQL: the CREATE statement plus its attachments, in
76
+ * source order. A matview whose source says `WITH NO DATA` gets a trailing
77
+ * REFRESH: the reconciler dropped a populated matview to rebuild it, and
78
+ * executing the source verbatim would leave it unpopulated — matching the
79
+ * text but regressing the database. (Found by a consumer whose extracted
80
+ * sources carry pg_dump's WITH NO DATA ordering.)
81
+ */
73
82
  function objectSql(obj: SourceObject): string[] {
74
- return [obj.sql, ...obj.attachments.map((a) => a.sql)];
83
+ const statements = [obj.sql, ...obj.attachments.map((a) => a.sql)];
84
+ if (obj.kind === 'materialized view' && WITH_NO_DATA_RE.test(normalizeSql(obj.sql))) {
85
+ statements.push(`REFRESH MATERIALIZED VIEW ${quoteQualified(obj.identity)}`);
86
+ }
87
+ return statements;
75
88
  }
76
89
 
77
90
  export function renderReconcileSql(plan: ReconcilePlan, source: SourceObject[]): RenderedReconcile {
package/src/cli/index.ts CHANGED
@@ -295,14 +295,14 @@ Usage:
295
295
  everystack db:backups [--stage <name>] List logical backups (id, size, created)
296
296
  everystack db:restore --from <id> [--stage <name>] --confirm Restore a backup INTO the stage's DB (DESTRUCTIVE)
297
297
  everystack db:backup:download <id> [--stage <name>] Presigned URL to download a backup's dump (valid 1h)
298
- everystack db:generate [--stage <name> | --database-url <url>] [--name <label>] [--models models/index.ts] [--allow-drops] Diff models vs the live DB → write the next migration (never touches the DB; DROPs held back unless --allow-drops)
298
+ everystack db:generate [--stage <name> | --database-url <url>] [--name <label>] [--models db/models/index.ts] [--allow-drops] Diff models vs the live DB → write the next migration (never touches the DB; DROPs held back unless --allow-drops)
299
299
  everystack db:pull [--stage <name> | --database-url <url>] [--schema public] [--out <dir | file.ts>] Introspect the live DB → render field() Models (the brownfield on-ramp). --out <dir> writes one file per model + index.ts (the default shape); --out <file.ts> writes a single module; stdout otherwise
300
300
  Both introspect via the deployed ops Lambda by default; --database-url (or an inherited DATABASE_URL) connects directly — for a schema that exists only on a local Postgres.
301
301
  everystack db:reconcile [--sql-dir db/sql] [--stage <name> | --database-url <url>] [--apply] [--check] [--baseline] [--overwrite-drift] [--json] Reconcile the derived layer (functions/views/matviews) against db/sql sources: plan with rebuild-cost estimates by default; --check is the CI gate; --apply executes (direct connection only) and records provenance + schema_log. Hand-edits are drift (never overwritten silently); first contact with existing objects wants --baseline, once.
302
302
  everystack db:authz:pull [--stage <name>] [--dir authz] Introspect live authz (rls/grants/policies/secdef) → reviewable contract files
303
303
  everystack db:authz:diff [--stage <name>] [--dir authz] Validate the live DB against the committed contract (non-zero exit on drift)
304
304
  everystack db:authz:test [--stage <name>] [--dir authz] Red-team enforcement: SET ROLE + attempt per role/table/command (non-zero exit on a hole)
305
- everystack db:authz:owner [--stage <name>] [--models models/index.ts] Red-team owner isolation: two JWT identities per owner-scoped table — catches IDOR (non-zero exit on a leak)
305
+ everystack db:authz:owner [--stage <name>] [--models db/models/index.ts] Red-team owner isolation: two JWT identities per owner-scoped table — catches IDOR (non-zero exit on a leak)
306
306
  everystack db:authz:report [--dir authz] Render the committed contract as a human-readable authorization review (no DB)
307
307
  everystack console --stage <name> [--sandbox] Interactive REPL on deployed Lambda
308
308
  everystack status [--stage <name>] [--hours <n>] Platform health: CDN, Lambda, rollup summary
@@ -0,0 +1,25 @@
1
+ /**
2
+ * models-path — where the Models barrel lives.
3
+ *
4
+ * One home for schema: `db/models/` (state) beside `db/sql/` (compute). The
5
+ * CLI looks there first, falling back to the legacy top-level `models/` so
6
+ * existing consumers keep working without a flag. An explicit `--models`
7
+ * always wins. When neither home exists, the NEW home is returned so error
8
+ * messages point people at the current convention, not the legacy one.
9
+ */
10
+
11
+ import { existsSync } from 'node:fs';
12
+
13
+ /** Candidate barrels, in preference order. */
14
+ export const MODELS_HOMES = ['db/models/index.ts', 'models/index.ts'] as const;
15
+
16
+ export function resolveModelsPath(
17
+ flag?: string,
18
+ exists: (p: string) => boolean = existsSync,
19
+ ): string {
20
+ if (flag) return flag;
21
+ for (const home of MODELS_HOMES) {
22
+ if (exists(home)) return home;
23
+ }
24
+ return MODELS_HOMES[0];
25
+ }
@@ -102,10 +102,12 @@ export function detectProjectReality(root: string): ProjectReality {
102
102
  ? 'V2'
103
103
  : 'V1';
104
104
 
105
- const modelsDir = path.join(abs, 'models');
105
+ // One home for schema: db/models is the current convention, top-level models the legacy one.
106
+ const modelsHome = isDir(path.join(abs, 'db', 'models')) ? 'db/models' : 'models';
107
+ const modelsDir = path.join(abs, modelsHome);
106
108
  const modelFiles = listTs(modelsDir).filter((f) => f !== 'index.ts');
107
109
  const models = {
108
- dir: isDir(modelsDir) ? 'models' : null,
110
+ dir: isDir(modelsDir) ? modelsHome : null,
109
111
  files: modelFiles,
110
112
  hasIndex: isFile(path.join(modelsDir, 'index.ts')),
111
113
  };