@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
|
@@ -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
|
|
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
|
|
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';
|
package/src/cli/derived-apply.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* the reconciler writes it and never reads it.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ?
|
|
110
|
+
dir: isDir(modelsDir) ? modelsHome : null,
|
|
109
111
|
files: modelFiles,
|
|
110
112
|
hasIndex: isFile(path.join(modelsDir, 'index.ts')),
|
|
111
113
|
};
|