@blamejs/core 0.7.4 → 0.7.18
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/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/db-declare-view.js
CHANGED
|
@@ -1,421 +1,420 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* b.db.declareView — declarative view + GRANT migration spec.
|
|
4
|
-
*
|
|
5
|
-
* Returns a migration-shape object that b.externalDb.migrate(...) applies
|
|
6
|
-
* against a Postgres backend. The view exposes a deliberately-narrowed
|
|
7
|
-
* column projection of the source table — sensitive columns are dropped
|
|
8
|
-
* (redactColumns), sealed columns are auto-omitted (operator declares
|
|
9
|
-
* via sealedColumns; a future externalDb sealed-fields registry will
|
|
10
|
-
* make this automatic), and existing derived hash columns can be
|
|
11
|
-
* exposed in place of their plaintext source via hashColumns.
|
|
12
|
-
*
|
|
13
|
-
* Postgres-only: SQLite has no GRANT semantics; MySQL's CREATE VIEW
|
|
14
|
-
* grammar differs and isn't covered. Apply throws NOT_SUPPORTED at
|
|
15
|
-
* migration-apply time when the targeted backend's dialect isn't
|
|
16
|
-
* "postgres" so operators see the failure cause clearly.
|
|
17
|
-
*
|
|
18
|
-
* Public API (b.db.declareView):
|
|
19
|
-
*
|
|
20
|
-
* var mig = b.db.declareView({
|
|
21
|
-
* schema: "analytics", // target schema
|
|
22
|
-
* name: "sessions", // view name
|
|
23
|
-
* source: "public.sessions", // schema.table OR ["public","sessions"]
|
|
24
|
-
* redactColumns: ["ssn", "diagnosis"], // dropped from view
|
|
25
|
-
* sealedColumns: ["secrets_jsonb"], // operator-declared sealed cols (auto-omit)
|
|
26
|
-
* hashColumns: { emailHash: "email" },// hide source plaintext, keep hash
|
|
27
|
-
* whereClause: "deleted_at IS NULL", // optional filter
|
|
28
|
-
* grantTo: ["analytics_user"], // roles that get SELECT
|
|
29
|
-
* backend: "main", // optional — defaults to default backend
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* // mig is a migration-shape object: { description, target, backend, up, down }
|
|
33
|
-
* // Place it in a migration file:
|
|
34
|
-
* // module.exports = mig;
|
|
35
|
-
* // and run b.externalDb.migrate.create({ dir }).up();
|
|
36
|
-
*
|
|
37
|
-
* Validation runs at migration apply (not declare) time so source-column
|
|
38
|
-
* existence and role existence checks query the live database. Operator
|
|
39
|
-
* typos surface as clear errors at the migrate command, not as silent
|
|
40
|
-
* empty views or grant-to-nonexistent-role footguns.
|
|
41
|
-
*
|
|
42
|
-
* Throw at declareView() call time on:
|
|
43
|
-
* - schema, name, source segments → safeSql.validateIdentifier
|
|
44
|
-
* - column names in redactColumns / sealedColumns / hashColumns →
|
|
45
|
-
* safeSql.validateIdentifier
|
|
46
|
-
* - role names in grantTo → safeSql.validateIdentifier
|
|
47
|
-
* - whereClause stays operator-supplied SQL (the framework cannot
|
|
48
|
-
* validate arbitrary expressions); it interpolates as-is into the
|
|
49
|
-
* migration text. Operators wrap any literal values inside
|
|
50
|
-
* whereClause via standard SQL quoting.
|
|
51
|
-
*
|
|
52
|
-
* Audit metadata emitted on apply:
|
|
53
|
-
* {
|
|
54
|
-
* view: "schema.name",
|
|
55
|
-
* source: "schema.table",
|
|
56
|
-
* selectedColumns: [string],
|
|
57
|
-
* redactedColumns: [string], // intersection of redactColumns & source
|
|
58
|
-
* autoExcludedSealed: [string], // sealedColumns members found in source
|
|
59
|
-
* hashedColumns: { aliasOrHashCol: srcCol },
|
|
60
|
-
* grantedTo: [string],
|
|
61
|
-
* }
|
|
62
|
-
*/
|
|
63
|
-
var safeSql = require("./safe-sql");
|
|
64
|
-
var validateOpts = require("./validate-opts");
|
|
65
|
-
var { defineClass } = require("./framework-error");
|
|
66
|
-
|
|
67
|
-
var DeclareViewError = defineClass("DeclareViewError", { alwaysPermanent: true });
|
|
68
|
-
|
|
69
|
-
var ALLOWED_OPTS = [
|
|
70
|
-
"schema", "name", "source",
|
|
71
|
-
"redactColumns", "sealedColumns", "hashColumns",
|
|
72
|
-
"whereClause", "grantTo", "backend",
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
function _err(code, message) {
|
|
76
|
-
return new DeclareViewError(code, message);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function _validateIdent(where, value) {
|
|
80
|
-
try {
|
|
81
|
-
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
82
|
-
} catch (e) {
|
|
83
|
-
throw _err("declare-view/bad-identifier",
|
|
84
|
-
where + ": invalid identifier '" + value + "': " + ((e && e.message) || String(e)));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function _validateStringArray(where, arr, optional) {
|
|
89
|
-
if (arr === undefined || arr === null) {
|
|
90
|
-
if (optional) return [];
|
|
91
|
-
throw _err("declare-view/missing-opt", where + " is required");
|
|
92
|
-
}
|
|
93
|
-
if (!Array.isArray(arr)) {
|
|
94
|
-
throw _err("declare-view/bad-type", where + " must be an array of strings");
|
|
95
|
-
}
|
|
96
|
-
for (var i = 0; i < arr.length; i++) {
|
|
97
|
-
if (typeof arr[i] !== "string" || arr[i].length === 0) {
|
|
98
|
-
throw _err("declare-view/bad-entry",
|
|
99
|
-
where + "[" + i + "] must be a non-empty string");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return arr.slice();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function _parseSource(source) {
|
|
106
|
-
// Accepts "schema.table" or ["schema", "table"] or "table" (defaults to public).
|
|
107
|
-
var parts;
|
|
108
|
-
if (typeof source === "string") {
|
|
109
|
-
if (source.length === 0) {
|
|
110
|
-
throw _err("declare-view/bad-source", "source must be a non-empty string or array");
|
|
111
|
-
}
|
|
112
|
-
parts = source.split(".");
|
|
113
|
-
} else if (Array.isArray(source)) {
|
|
114
|
-
parts = source.slice();
|
|
115
|
-
} else {
|
|
116
|
-
throw _err("declare-view/bad-source",
|
|
117
|
-
"source must be a string ('schema.table') or array (['schema','table']), got " + typeof source);
|
|
118
|
-
}
|
|
119
|
-
if (parts.length === 1) parts = ["public", parts[0]];
|
|
120
|
-
if (parts.length !== 2) {
|
|
121
|
-
throw _err("declare-view/bad-source",
|
|
122
|
-
"source must resolve to two segments [schema, table]; got " + parts.length + " segment(s)");
|
|
123
|
-
}
|
|
124
|
-
for (var i = 0; i < parts.length; i++) _validateIdent("source[" + i + "]", parts[i]);
|
|
125
|
-
return { schema: parts[0], name: parts[1] };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function _validateOpts(opts) {
|
|
129
|
-
if (!opts || typeof opts !== "object") {
|
|
130
|
-
throw _err("declare-view/bad-opts", "declareView requires an opts object");
|
|
131
|
-
}
|
|
132
|
-
for (var k in opts) {
|
|
133
|
-
if (Object.prototype.hasOwnProperty.call(opts, k) && ALLOWED_OPTS.indexOf(k) === -1) {
|
|
134
|
-
throw _err("declare-view/unknown-opt",
|
|
135
|
-
"unknown opt '" + k + "'. Allowed: " + ALLOWED_OPTS.join(", "));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareViewError, "declare-view/missing-opt");
|
|
140
|
-
_validateIdent("schema", opts.schema);
|
|
141
|
-
|
|
142
|
-
validateOpts.requireNonEmptyString(opts.name, "name", DeclareViewError, "declare-view/missing-opt");
|
|
143
|
-
_validateIdent("name", opts.name);
|
|
144
|
-
|
|
145
|
-
if (opts.source === undefined) {
|
|
146
|
-
throw _err("declare-view/missing-opt", "source is required");
|
|
147
|
-
}
|
|
148
|
-
var src = _parseSource(opts.source);
|
|
149
|
-
|
|
150
|
-
var redactColumns = _validateStringArray("redactColumns", opts.redactColumns, true);
|
|
151
|
-
for (var i = 0; i < redactColumns.length; i++) {
|
|
152
|
-
_validateIdent("redactColumns[" + i + "]", redactColumns[i]);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
var sealedColumns = _validateStringArray("sealedColumns", opts.sealedColumns, true);
|
|
156
|
-
for (var j = 0; j < sealedColumns.length; j++) {
|
|
157
|
-
_validateIdent("sealedColumns[" + j + "]", sealedColumns[j]);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
var hashColumns = {};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
var
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
var
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
var
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
var
|
|
250
|
-
for (var
|
|
251
|
-
for (var
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
var
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
var
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
var
|
|
274
|
-
var
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
var
|
|
299
|
-
var
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
var
|
|
340
|
-
var
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
"
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
var
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.db.declareView — declarative view + GRANT migration spec.
|
|
4
|
+
*
|
|
5
|
+
* Returns a migration-shape object that b.externalDb.migrate(...) applies
|
|
6
|
+
* against a Postgres backend. The view exposes a deliberately-narrowed
|
|
7
|
+
* column projection of the source table — sensitive columns are dropped
|
|
8
|
+
* (redactColumns), sealed columns are auto-omitted (operator declares
|
|
9
|
+
* via sealedColumns; a future externalDb sealed-fields registry will
|
|
10
|
+
* make this automatic), and existing derived hash columns can be
|
|
11
|
+
* exposed in place of their plaintext source via hashColumns.
|
|
12
|
+
*
|
|
13
|
+
* Postgres-only: SQLite has no GRANT semantics; MySQL's CREATE VIEW
|
|
14
|
+
* grammar differs and isn't covered. Apply throws NOT_SUPPORTED at
|
|
15
|
+
* migration-apply time when the targeted backend's dialect isn't
|
|
16
|
+
* "postgres" so operators see the failure cause clearly.
|
|
17
|
+
*
|
|
18
|
+
* Public API (b.db.declareView):
|
|
19
|
+
*
|
|
20
|
+
* var mig = b.db.declareView({
|
|
21
|
+
* schema: "analytics", // target schema
|
|
22
|
+
* name: "sessions", // view name
|
|
23
|
+
* source: "public.sessions", // schema.table OR ["public","sessions"]
|
|
24
|
+
* redactColumns: ["ssn", "diagnosis"], // dropped from view
|
|
25
|
+
* sealedColumns: ["secrets_jsonb"], // operator-declared sealed cols (auto-omit)
|
|
26
|
+
* hashColumns: { emailHash: "email" },// hide source plaintext, keep hash
|
|
27
|
+
* whereClause: "deleted_at IS NULL", // optional filter
|
|
28
|
+
* grantTo: ["analytics_user"], // roles that get SELECT
|
|
29
|
+
* backend: "main", // optional — defaults to default backend
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // mig is a migration-shape object: { description, target, backend, up, down }
|
|
33
|
+
* // Place it in a migration file:
|
|
34
|
+
* // module.exports = mig;
|
|
35
|
+
* // and run b.externalDb.migrate.create({ dir }).up();
|
|
36
|
+
*
|
|
37
|
+
* Validation runs at migration apply (not declare) time so source-column
|
|
38
|
+
* existence and role existence checks query the live database. Operator
|
|
39
|
+
* typos surface as clear errors at the migrate command, not as silent
|
|
40
|
+
* empty views or grant-to-nonexistent-role footguns.
|
|
41
|
+
*
|
|
42
|
+
* Throw at declareView() call time on:
|
|
43
|
+
* - schema, name, source segments → safeSql.validateIdentifier
|
|
44
|
+
* - column names in redactColumns / sealedColumns / hashColumns →
|
|
45
|
+
* safeSql.validateIdentifier
|
|
46
|
+
* - role names in grantTo → safeSql.validateIdentifier
|
|
47
|
+
* - whereClause stays operator-supplied SQL (the framework cannot
|
|
48
|
+
* validate arbitrary expressions); it interpolates as-is into the
|
|
49
|
+
* migration text. Operators wrap any literal values inside
|
|
50
|
+
* whereClause via standard SQL quoting.
|
|
51
|
+
*
|
|
52
|
+
* Audit metadata emitted on apply:
|
|
53
|
+
* {
|
|
54
|
+
* view: "schema.name",
|
|
55
|
+
* source: "schema.table",
|
|
56
|
+
* selectedColumns: [string],
|
|
57
|
+
* redactedColumns: [string], // intersection of redactColumns & source
|
|
58
|
+
* autoExcludedSealed: [string], // sealedColumns members found in source
|
|
59
|
+
* hashedColumns: { aliasOrHashCol: srcCol },
|
|
60
|
+
* grantedTo: [string],
|
|
61
|
+
* }
|
|
62
|
+
*/
|
|
63
|
+
var safeSql = require("./safe-sql");
|
|
64
|
+
var validateOpts = require("./validate-opts");
|
|
65
|
+
var { defineClass } = require("./framework-error");
|
|
66
|
+
|
|
67
|
+
var DeclareViewError = defineClass("DeclareViewError", { alwaysPermanent: true });
|
|
68
|
+
|
|
69
|
+
var ALLOWED_OPTS = [
|
|
70
|
+
"schema", "name", "source",
|
|
71
|
+
"redactColumns", "sealedColumns", "hashColumns",
|
|
72
|
+
"whereClause", "grantTo", "backend",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function _err(code, message) {
|
|
76
|
+
return new DeclareViewError(code, message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _validateIdent(where, value) {
|
|
80
|
+
try {
|
|
81
|
+
safeSql.validateIdentifier(value, { allowReserved: true });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
throw _err("declare-view/bad-identifier",
|
|
84
|
+
where + ": invalid identifier '" + value + "': " + ((e && e.message) || String(e)));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _validateStringArray(where, arr, optional) {
|
|
89
|
+
if (arr === undefined || arr === null) {
|
|
90
|
+
if (optional) return [];
|
|
91
|
+
throw _err("declare-view/missing-opt", where + " is required");
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(arr)) {
|
|
94
|
+
throw _err("declare-view/bad-type", where + " must be an array of strings");
|
|
95
|
+
}
|
|
96
|
+
for (var i = 0; i < arr.length; i++) {
|
|
97
|
+
if (typeof arr[i] !== "string" || arr[i].length === 0) {
|
|
98
|
+
throw _err("declare-view/bad-entry",
|
|
99
|
+
where + "[" + i + "] must be a non-empty string");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return arr.slice();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _parseSource(source) {
|
|
106
|
+
// Accepts "schema.table" or ["schema", "table"] or "table" (defaults to public).
|
|
107
|
+
var parts;
|
|
108
|
+
if (typeof source === "string") {
|
|
109
|
+
if (source.length === 0) {
|
|
110
|
+
throw _err("declare-view/bad-source", "source must be a non-empty string or array");
|
|
111
|
+
}
|
|
112
|
+
parts = source.split(".");
|
|
113
|
+
} else if (Array.isArray(source)) {
|
|
114
|
+
parts = source.slice();
|
|
115
|
+
} else {
|
|
116
|
+
throw _err("declare-view/bad-source",
|
|
117
|
+
"source must be a string ('schema.table') or array (['schema','table']), got " + typeof source);
|
|
118
|
+
}
|
|
119
|
+
if (parts.length === 1) parts = ["public", parts[0]];
|
|
120
|
+
if (parts.length !== 2) {
|
|
121
|
+
throw _err("declare-view/bad-source",
|
|
122
|
+
"source must resolve to two segments [schema, table]; got " + parts.length + " segment(s)");
|
|
123
|
+
}
|
|
124
|
+
for (var i = 0; i < parts.length; i++) _validateIdent("source[" + i + "]", parts[i]);
|
|
125
|
+
return { schema: parts[0], name: parts[1] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _validateOpts(opts) {
|
|
129
|
+
if (!opts || typeof opts !== "object") {
|
|
130
|
+
throw _err("declare-view/bad-opts", "declareView requires an opts object");
|
|
131
|
+
}
|
|
132
|
+
for (var k in opts) {
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(opts, k) && ALLOWED_OPTS.indexOf(k) === -1) {
|
|
134
|
+
throw _err("declare-view/unknown-opt",
|
|
135
|
+
"unknown opt '" + k + "'. Allowed: " + ALLOWED_OPTS.join(", "));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
validateOpts.requireNonEmptyString(opts.schema, "schema", DeclareViewError, "declare-view/missing-opt");
|
|
140
|
+
_validateIdent("schema", opts.schema);
|
|
141
|
+
|
|
142
|
+
validateOpts.requireNonEmptyString(opts.name, "name", DeclareViewError, "declare-view/missing-opt");
|
|
143
|
+
_validateIdent("name", opts.name);
|
|
144
|
+
|
|
145
|
+
if (opts.source === undefined) {
|
|
146
|
+
throw _err("declare-view/missing-opt", "source is required");
|
|
147
|
+
}
|
|
148
|
+
var src = _parseSource(opts.source);
|
|
149
|
+
|
|
150
|
+
var redactColumns = _validateStringArray("redactColumns", opts.redactColumns, true);
|
|
151
|
+
for (var i = 0; i < redactColumns.length; i++) {
|
|
152
|
+
_validateIdent("redactColumns[" + i + "]", redactColumns[i]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var sealedColumns = _validateStringArray("sealedColumns", opts.sealedColumns, true);
|
|
156
|
+
for (var j = 0; j < sealedColumns.length; j++) {
|
|
157
|
+
_validateIdent("sealedColumns[" + j + "]", sealedColumns[j]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var hashColumns = {};
|
|
161
|
+
validateOpts.optionalPlainObject(opts.hashColumns, "hashColumns",
|
|
162
|
+
DeclareViewError, "declare-view/bad-type",
|
|
163
|
+
"must be an object { aliasOrHashCol: srcCol }");
|
|
164
|
+
if (opts.hashColumns !== undefined && opts.hashColumns !== null) {
|
|
165
|
+
for (var hc in opts.hashColumns) {
|
|
166
|
+
if (!Object.prototype.hasOwnProperty.call(opts.hashColumns, hc)) continue;
|
|
167
|
+
_validateIdent("hashColumns key '" + hc + "'", hc);
|
|
168
|
+
var v = opts.hashColumns[hc];
|
|
169
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
170
|
+
throw _err("declare-view/bad-entry",
|
|
171
|
+
"hashColumns['" + hc + "'] must be a non-empty string (the source plaintext column)");
|
|
172
|
+
}
|
|
173
|
+
_validateIdent("hashColumns['" + hc + "']", v);
|
|
174
|
+
hashColumns[hc] = v;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var whereClause = null;
|
|
179
|
+
if (opts.whereClause !== undefined && opts.whereClause !== null) {
|
|
180
|
+
if (typeof opts.whereClause !== "string") {
|
|
181
|
+
throw _err("declare-view/bad-type", "whereClause must be a string");
|
|
182
|
+
}
|
|
183
|
+
if (opts.whereClause.indexOf(";") !== -1) {
|
|
184
|
+
throw _err("declare-view/bad-where",
|
|
185
|
+
"whereClause must not contain ';' — use a single boolean expression");
|
|
186
|
+
}
|
|
187
|
+
whereClause = opts.whereClause;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var grantTo = _validateStringArray("grantTo", opts.grantTo, true);
|
|
191
|
+
for (var g = 0; g < grantTo.length; g++) {
|
|
192
|
+
_validateIdent("grantTo[" + g + "]", grantTo[g]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (opts.backend !== undefined && opts.backend !== null) {
|
|
196
|
+
if (typeof opts.backend !== "string" || opts.backend.length === 0) {
|
|
197
|
+
throw _err("declare-view/bad-type", "backend must be a non-empty string");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
schema: opts.schema,
|
|
203
|
+
name: opts.name,
|
|
204
|
+
source: src,
|
|
205
|
+
redactColumns: redactColumns,
|
|
206
|
+
sealedColumns: sealedColumns,
|
|
207
|
+
hashColumns: hashColumns,
|
|
208
|
+
whereClause: whereClause,
|
|
209
|
+
grantTo: grantTo,
|
|
210
|
+
backend: opts.backend || null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- Apply-time helpers (run inside up()) ----
|
|
215
|
+
|
|
216
|
+
async function _fetchSourceColumns(xdb, schema, table) {
|
|
217
|
+
var res = await xdb.query(
|
|
218
|
+
"SELECT column_name FROM information_schema.columns " +
|
|
219
|
+
"WHERE table_schema = $1 AND table_name = $2 " +
|
|
220
|
+
"ORDER BY ordinal_position ASC",
|
|
221
|
+
[schema, table]
|
|
222
|
+
);
|
|
223
|
+
var rows = (res && res.rows) || [];
|
|
224
|
+
return rows.map(function (r) { return r.column_name; });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function _fetchExistingRoles(xdb, names) {
|
|
228
|
+
if (names.length === 0) return new Set();
|
|
229
|
+
var placeholders = names.map(function (_, i) { return "$" + (i + 1); }).join(", ");
|
|
230
|
+
var res = await xdb.query(
|
|
231
|
+
"SELECT rolname FROM pg_roles WHERE rolname IN (" + placeholders + ")",
|
|
232
|
+
names
|
|
233
|
+
);
|
|
234
|
+
var rows = (res && res.rows) || [];
|
|
235
|
+
return new Set(rows.map(function (r) { return r.rolname; }));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _missing(required, available) {
|
|
239
|
+
var miss = [];
|
|
240
|
+
for (var i = 0; i < required.length; i++) {
|
|
241
|
+
if (available.indexOf(required[i]) === -1) miss.push(required[i]);
|
|
242
|
+
}
|
|
243
|
+
return miss;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _buildSelectColumnList(sourceCols, spec) {
|
|
247
|
+
// Drop set: redactColumns ∩ source, sealedColumns ∩ source, hashColumns.values ∩ source.
|
|
248
|
+
var dropSet = Object.create(null);
|
|
249
|
+
for (var i = 0; i < spec.redactColumns.length; i++) dropSet[spec.redactColumns[i]] = true;
|
|
250
|
+
for (var j = 0; j < spec.sealedColumns.length; j++) dropSet[spec.sealedColumns[j]] = true;
|
|
251
|
+
for (var hc in spec.hashColumns) dropSet[spec.hashColumns[hc]] = true;
|
|
252
|
+
|
|
253
|
+
var kept = [];
|
|
254
|
+
for (var k = 0; k < sourceCols.length; k++) {
|
|
255
|
+
if (!dropSet[sourceCols[k]]) kept.push(sourceCols[k]);
|
|
256
|
+
}
|
|
257
|
+
return kept;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _intersectInSource(list, sourceCols) {
|
|
261
|
+
var srcSet = Object.create(null);
|
|
262
|
+
for (var i = 0; i < sourceCols.length; i++) srcSet[sourceCols[i]] = true;
|
|
263
|
+
var out = [];
|
|
264
|
+
for (var j = 0; j < list.length; j++) {
|
|
265
|
+
if (srcSet[list[j]]) out.push(list[j]);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _ensureBackendIsPostgres(externalDb, backendName) {
|
|
271
|
+
// externalDb.listBackends() returns { name, dialect, ... } per backend.
|
|
272
|
+
var list = externalDb.listBackends();
|
|
273
|
+
var found = null;
|
|
274
|
+
for (var i = 0; i < list.length; i++) {
|
|
275
|
+
if (list[i].name === backendName) { found = list[i]; break; }
|
|
276
|
+
}
|
|
277
|
+
if (!found) {
|
|
278
|
+
throw _err("declare-view/unknown-backend",
|
|
279
|
+
"no externalDb backend named '" + backendName + "' — declared backends: " +
|
|
280
|
+
list.map(function (b) { return b.name; }).join(", "));
|
|
281
|
+
}
|
|
282
|
+
if (found.dialect !== "postgres") {
|
|
283
|
+
throw _err("declare-view/not-supported",
|
|
284
|
+
"declareView is Postgres-only; backend '" + backendName + "' has dialect='" +
|
|
285
|
+
found.dialect + "'. Write the view as a hand-rolled migration for this dialect.");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---- The factory ----
|
|
290
|
+
|
|
291
|
+
function declareView(opts) {
|
|
292
|
+
var spec = _validateOpts(opts);
|
|
293
|
+
|
|
294
|
+
// The migration shape consumed by b.externalDb.migrate. The runner
|
|
295
|
+
// resolves the backend at apply time (operator may set spec.backend
|
|
296
|
+
// explicitly OR rely on the migrate runner's default backend).
|
|
297
|
+
var description = "declareView " + spec.schema + "." + spec.name;
|
|
298
|
+
var qView = safeSql.quoteQualified([spec.schema, spec.name], "postgres");
|
|
299
|
+
var qSource = safeSql.quoteQualified([spec.source.schema, spec.source.name], "postgres");
|
|
300
|
+
|
|
301
|
+
async function up(xdb, ctx) {
|
|
302
|
+
// Boundary throw: confirm we're on Postgres before any DDL leaves the process.
|
|
303
|
+
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
304
|
+
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Live validation — source columns + roles must exist.
|
|
308
|
+
var sourceCols = await _fetchSourceColumns(xdb, spec.source.schema, spec.source.name);
|
|
309
|
+
if (sourceCols.length === 0) {
|
|
310
|
+
throw _err("declare-view/source-not-found",
|
|
311
|
+
"source table '" + spec.source.schema + "." + spec.source.name +
|
|
312
|
+
"' has no columns visible (does it exist? does the migration role have SELECT on information_schema.columns?)");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var missingRedact = _missing(spec.redactColumns, sourceCols);
|
|
316
|
+
if (missingRedact.length > 0) {
|
|
317
|
+
throw _err("declare-view/redact-not-in-source",
|
|
318
|
+
"redactColumns [" + missingRedact.join(", ") + "] not present on source '" +
|
|
319
|
+
spec.source.schema + "." + spec.source.name + "'");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
var missingHashKeys = _missing(Object.keys(spec.hashColumns), sourceCols);
|
|
323
|
+
if (missingHashKeys.length > 0) {
|
|
324
|
+
throw _err("declare-view/hash-key-not-in-source",
|
|
325
|
+
"hashColumns keys [" + missingHashKeys.join(", ") + "] not present on source '" +
|
|
326
|
+
spec.source.schema + "." + spec.source.name +
|
|
327
|
+
"' — declare them as derivedHashes on the source schema first");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var missingHashSrc = _missing(_objValues(spec.hashColumns), sourceCols);
|
|
331
|
+
if (missingHashSrc.length > 0) {
|
|
332
|
+
throw _err("declare-view/hash-source-not-in-source",
|
|
333
|
+
"hashColumns values [" + missingHashSrc.join(", ") + "] not present on source '" +
|
|
334
|
+
spec.source.schema + "." + spec.source.name + "'");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (spec.grantTo.length > 0) {
|
|
338
|
+
var existing = await _fetchExistingRoles(xdb, spec.grantTo);
|
|
339
|
+
var missingRoles = [];
|
|
340
|
+
for (var i = 0; i < spec.grantTo.length; i++) {
|
|
341
|
+
if (!existing.has(spec.grantTo[i])) missingRoles.push(spec.grantTo[i]);
|
|
342
|
+
}
|
|
343
|
+
if (missingRoles.length > 0) {
|
|
344
|
+
throw _err("declare-view/role-not-found",
|
|
345
|
+
"grantTo roles [" + missingRoles.join(", ") +
|
|
346
|
+
"] not present in pg_roles — CREATE ROLE them in an earlier migration");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
var selectedColumns = _buildSelectColumnList(sourceCols, spec);
|
|
351
|
+
if (selectedColumns.length === 0) {
|
|
352
|
+
throw _err("declare-view/empty-select",
|
|
353
|
+
"view '" + spec.schema + "." + spec.name +
|
|
354
|
+
"' would have zero columns after redact/sealed/hash exclusions — adjust the spec");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Build CREATE VIEW. Each column is independently quoted so a
|
|
358
|
+
// reserved-word column name (e.g. "user", "order") resolves correctly.
|
|
359
|
+
var quotedCols = selectedColumns.map(function (c) {
|
|
360
|
+
return safeSql.quoteIdentifier(c, "postgres");
|
|
361
|
+
}).join(", ");
|
|
362
|
+
var createSql = "CREATE VIEW " + qView + " AS SELECT " + quotedCols +
|
|
363
|
+
" FROM " + qSource;
|
|
364
|
+
if (spec.whereClause) createSql += " WHERE " + spec.whereClause;
|
|
365
|
+
|
|
366
|
+
await xdb.query(createSql, []);
|
|
367
|
+
|
|
368
|
+
// GRANT SELECT — one statement covers all roles.
|
|
369
|
+
if (spec.grantTo.length > 0) {
|
|
370
|
+
var quotedRoles = spec.grantTo.map(function (r) {
|
|
371
|
+
return safeSql.quoteIdentifier(r, "postgres");
|
|
372
|
+
}).join(", ");
|
|
373
|
+
await xdb.query(
|
|
374
|
+
"GRANT SELECT ON " + qView + " TO " + quotedRoles,
|
|
375
|
+
[]
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
view: spec.schema + "." + spec.name,
|
|
381
|
+
source: spec.source.schema + "." + spec.source.name,
|
|
382
|
+
selectedColumns: selectedColumns,
|
|
383
|
+
redactedColumns: _intersectInSource(spec.redactColumns, sourceCols),
|
|
384
|
+
autoExcludedSealed: _intersectInSource(spec.sealedColumns, sourceCols),
|
|
385
|
+
hashedColumns: Object.assign({}, spec.hashColumns),
|
|
386
|
+
grantedTo: spec.grantTo.slice(),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function down(xdb, ctx) {
|
|
391
|
+
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
392
|
+
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
393
|
+
}
|
|
394
|
+
await xdb.query("DROP VIEW IF EXISTS " + qView, []);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
description: description,
|
|
399
|
+
target: "externalDb",
|
|
400
|
+
backend: spec.backend,
|
|
401
|
+
up: up,
|
|
402
|
+
down: down,
|
|
403
|
+
// Expose the validated spec for testability — declareView callers
|
|
404
|
+
// can introspect what the migration will emit without running it.
|
|
405
|
+
_spec: spec,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function _objValues(obj) {
|
|
410
|
+
var out = [];
|
|
411
|
+
for (var k in obj) {
|
|
412
|
+
if (Object.prototype.hasOwnProperty.call(obj, k)) out.push(obj[k]);
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
declareView: declareView,
|
|
419
|
+
DeclareViewError: DeclareViewError,
|
|
420
|
+
};
|