@dudousxd/nestjs-catalog 0.13.0 → 0.15.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.
- package/dist/catalog.controller.js +66 -6
- package/dist/catalog.csv.d.ts +89 -0
- package/dist/catalog.csv.js +163 -0
- package/dist/catalog.filters.d.ts +174 -0
- package/dist/catalog.filters.js +272 -0
- package/dist/catalog.identifiers.d.ts +161 -0
- package/dist/catalog.identifiers.js +195 -0
- package/dist/catalog.pipeline.d.ts +392 -35
- package/dist/catalog.pipeline.js +175 -25
- package/dist/catalog.query-cache.d.ts +0 -2
- package/dist/catalog.query-cache.js +0 -18
- package/dist/catalog.query.d.ts +43 -0
- package/dist/catalog.query.js +5 -0
- package/dist/catalog.service.d.ts +51 -0
- package/dist/catalog.service.js +126 -3
- package/dist/catalog.store.d.ts +84 -22
- package/dist/catalog.store.js +36 -68
- package/dist/catalog.types.d.ts +64 -0
- package/dist/client.d.ts +70 -2
- package/dist/client.js +66 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +24 -3
- package/dist/stores/mikro-orm-read.store.d.ts +8 -2
- package/dist/stores/mikro-orm-read.store.js +77 -11
- package/package.json +1 -1
package/dist/catalog.service.js
CHANGED
|
@@ -11,10 +11,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
|
+
var CatalogService_1;
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
16
|
exports.CatalogService = void 0;
|
|
16
17
|
const common_1 = require("@nestjs/common");
|
|
17
18
|
const catalog_events_1 = require("./catalog.events");
|
|
19
|
+
const catalog_filters_1 = require("./catalog.filters");
|
|
18
20
|
const catalog_options_1 = require("./catalog.options");
|
|
19
21
|
const catalog_query_1 = require("./catalog.query");
|
|
20
22
|
const catalog_query_cache_1 = require("./catalog.query-cache");
|
|
@@ -24,6 +26,17 @@ const catalog_workspace_1 = require("./catalog.workspace");
|
|
|
24
26
|
const search_1 = require("./search");
|
|
25
27
|
const DEFAULT_PAGE_SIZE = 25;
|
|
26
28
|
const DEFAULT_MAX_PAGE_SIZE = 200;
|
|
29
|
+
/**
|
|
30
|
+
* A finished array, seen as the shape a streamed read has.
|
|
31
|
+
*
|
|
32
|
+
* Yields out of the caller's array rather than copying it: the rows are already
|
|
33
|
+
* held by whoever produced them, and a second copy would be the cost this whole
|
|
34
|
+
* path exists to avoid, paid on the one code path that could least afford it.
|
|
35
|
+
*/
|
|
36
|
+
async function* fromRows(rows) {
|
|
37
|
+
for (const row of rows)
|
|
38
|
+
yield row;
|
|
39
|
+
}
|
|
27
40
|
/**
|
|
28
41
|
* Reads objects of any catalogued type through one endpoint.
|
|
29
42
|
*
|
|
@@ -33,7 +46,7 @@ const DEFAULT_MAX_PAGE_SIZE = 200;
|
|
|
33
46
|
* new store cannot accidentally relax them — the appeal of a generic read
|
|
34
47
|
* endpoint is also its whole risk.
|
|
35
48
|
*/
|
|
36
|
-
let CatalogService = class CatalogService {
|
|
49
|
+
let CatalogService = CatalogService_1 = class CatalogService {
|
|
37
50
|
registry;
|
|
38
51
|
store;
|
|
39
52
|
options;
|
|
@@ -45,6 +58,7 @@ let CatalogService = class CatalogService {
|
|
|
45
58
|
this.workspace = workspace;
|
|
46
59
|
}
|
|
47
60
|
cache = new catalog_query_cache_1.QueryCache();
|
|
61
|
+
logger = new common_1.Logger(CatalogService_1.name);
|
|
48
62
|
// ---------------------------------------------------------------------------
|
|
49
63
|
// The facade.
|
|
50
64
|
//
|
|
@@ -130,14 +144,24 @@ let CatalogService = class CatalogService {
|
|
|
130
144
|
// Sort is validated here rather than in the store: an unrecognised column
|
|
131
145
|
// must never reach a query builder, whatever the engine.
|
|
132
146
|
const sort = columns.some((c) => c.name === query.sort) ? query.sort : undefined;
|
|
133
|
-
|
|
147
|
+
// Filters, against the same `columns` a sort is checked against and for the
|
|
148
|
+
// same reason — with one difference in what a failure means. An unrecognised
|
|
149
|
+
// sort falls back to the primary key, because the rows are the same rows in a
|
|
150
|
+
// different order. An unrecognised filter cannot fall back to anything: the
|
|
151
|
+
// read would come back holding rows the caller asked to exclude, and neither
|
|
152
|
+
// the caller nor the screen has any way to tell.
|
|
153
|
+
const filters = this.resolveFilters(columns, query.filters ?? []);
|
|
154
|
+
const result = await this.store.read(type, fields, {
|
|
134
155
|
page,
|
|
135
156
|
size,
|
|
136
157
|
search: query.search,
|
|
137
158
|
sort,
|
|
138
159
|
dir: query.dir === 'desc' ? 'desc' : 'asc',
|
|
139
160
|
snapshot: query.snapshot,
|
|
161
|
+
...(filters.length > 0 ? { filters } : {}),
|
|
140
162
|
});
|
|
163
|
+
const { rows, total } = result;
|
|
164
|
+
const storeOperators = this.filterOperators();
|
|
141
165
|
return {
|
|
142
166
|
type: type.name,
|
|
143
167
|
page,
|
|
@@ -150,10 +174,52 @@ let CatalogService = class CatalogService {
|
|
|
150
174
|
type: c.type,
|
|
151
175
|
classification: c.classification,
|
|
152
176
|
unit: c.unit,
|
|
177
|
+
columnName: c.columnName,
|
|
178
|
+
// What this deployment will actually accept for this column: the rule
|
|
179
|
+
// derived from the column, narrowed by what the mounted store can do.
|
|
180
|
+
// Sent per column so a console needs no second request and no table of
|
|
181
|
+
// its own — see `catalog.filters.ts` on why a hand-kept list is the
|
|
182
|
+
// failure mode being avoided.
|
|
183
|
+
filterOperators: (0, catalog_filters_1.offeredFilterOperators)(c, storeOperators),
|
|
153
184
|
})),
|
|
154
185
|
rows,
|
|
186
|
+
...(result.snapshot ? { snapshot: result.snapshot } : {}),
|
|
155
187
|
};
|
|
156
188
|
}
|
|
189
|
+
/** What the mounted store can push into a read predicate. Empty when it cannot. */
|
|
190
|
+
filterOperators() {
|
|
191
|
+
return (0, catalog_store_1.supportsObjectFilters)(this.store) ? this.store.objectFilterOperators : [];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every filter, or a refusal naming all of them at once.
|
|
195
|
+
*
|
|
196
|
+
* One message listing every problem rather than the first: somebody who built
|
|
197
|
+
* four filters and got two of them wrong should learn that in one round trip.
|
|
198
|
+
*
|
|
199
|
+
* The store is asked whether it can honour the operators before the read runs,
|
|
200
|
+
* which is what stops a store that does not filter from answering with an
|
|
201
|
+
* unfiltered page. That refusal is worth more than it costs — a screen only
|
|
202
|
+
* offers what `filterOperators` reported, so a caller reaching this branch is
|
|
203
|
+
* one that built the request itself.
|
|
204
|
+
*/
|
|
205
|
+
resolveFilters(columns, raw) {
|
|
206
|
+
if (raw.length === 0)
|
|
207
|
+
return [];
|
|
208
|
+
const { filters, problems } = (0, catalog_filters_1.resolveObjectFilters)(columns, raw);
|
|
209
|
+
const supported = this.filterOperators();
|
|
210
|
+
const unsupported = filters
|
|
211
|
+
.map((filter) => filter.op)
|
|
212
|
+
.filter((op) => !supported.some((available) => available === op));
|
|
213
|
+
if (unsupported.length > 0) {
|
|
214
|
+
throw new common_1.BadRequestException(supported.length === 0
|
|
215
|
+
? "This catalog's store does not filter object reads, so it can only be paged, searched and sorted."
|
|
216
|
+
: `This catalog's store cannot filter with ${[...new Set(unsupported)].join(', ')}. It applies ${supported.join(', ')}.`);
|
|
217
|
+
}
|
|
218
|
+
if (problems.length > 0) {
|
|
219
|
+
throw new common_1.BadRequestException(problems.join(' '));
|
|
220
|
+
}
|
|
221
|
+
return filters;
|
|
222
|
+
}
|
|
157
223
|
/** Empty when the store keeps no history. */
|
|
158
224
|
async listSnapshots(typeName) {
|
|
159
225
|
const type = this.registry.getType(typeName);
|
|
@@ -366,6 +432,63 @@ let CatalogService = class CatalogService {
|
|
|
366
432
|
});
|
|
367
433
|
return { savedQuery: saved, result };
|
|
368
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* The same saved query, as rows arriving rather than a result.
|
|
437
|
+
*
|
|
438
|
+
* For the export route, and it differs from {@link runSavedQuery} in three
|
|
439
|
+
* ways that are all the same decision seen from different sides.
|
|
440
|
+
*
|
|
441
|
+
* **No cap when the store streams.** An export is the whole result by
|
|
442
|
+
* definition — that is what distinguishes it from the table it was exported
|
|
443
|
+
* from — so `maxQueryRows` is not applied. It cannot be: a capped export is a
|
|
444
|
+
* prefix handed over as a complete file, with nothing in the file to say so.
|
|
445
|
+
*
|
|
446
|
+
* **No cache, in either direction.** Nothing is read from it, because what it
|
|
447
|
+
* holds is a *capped* page and serving that would silently truncate; and
|
|
448
|
+
* nothing is written to it, because the thing being produced is the object the
|
|
449
|
+
* cache exists to avoid holding.
|
|
450
|
+
*
|
|
451
|
+
* **No timeout.** {@link CatalogModuleOptions.queryTimeoutMs} bounds a
|
|
452
|
+
* statement somebody is waiting on behind a screen. An export of a large table
|
|
453
|
+
* runs for as long as the table takes, and the bound that matters is that
|
|
454
|
+
* neither this process nor the driver holds more than a row — which the stream
|
|
455
|
+
* is what provides. A client that gives up closes the connection, and the
|
|
456
|
+
* consumer stopping its pull is what releases the read.
|
|
457
|
+
*
|
|
458
|
+
* **A store that cannot stream falls back to the capped buffered read**, and
|
|
459
|
+
* that is a real difference in what the same route returns depending on what
|
|
460
|
+
* is mounted underneath. It is the honest option: lifting the cap on a store
|
|
461
|
+
* that materialises its result set would not make the export complete, it
|
|
462
|
+
* would move the failure into the driver, where there is no cap to report. The
|
|
463
|
+
* truncation is logged, since a CSV has nowhere to carry the fact.
|
|
464
|
+
*/
|
|
465
|
+
async streamSavedQuery(id) {
|
|
466
|
+
const saved = await this.getSavedQuery(id);
|
|
467
|
+
if (!(0, catalog_query_1.isQueryStore)(this.store)) {
|
|
468
|
+
throw new common_1.BadRequestException("This catalog's store does not support SQL queries.");
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
(0, catalog_query_1.assertReadOnlyShape)(saved.sql);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
throw new common_1.BadRequestException(error instanceof Error ? error.message : String(error));
|
|
475
|
+
}
|
|
476
|
+
if ((0, catalog_query_1.isStreamingQueryStore)(this.store)) {
|
|
477
|
+
// Columns are left undefined: a streamed read learns them from its first
|
|
478
|
+
// row, exactly as the buffered one does from `rows[0]`.
|
|
479
|
+
return { savedQuery: saved, rows: this.store.streamQuery({ sql: saved.sql }) };
|
|
480
|
+
}
|
|
481
|
+
const cap = this.options.maxQueryRows ?? 1_000;
|
|
482
|
+
const result = await this.store.runQuery({
|
|
483
|
+
sql: saved.sql,
|
|
484
|
+
maxRows: cap,
|
|
485
|
+
timeoutMs: this.options.queryTimeoutMs ?? 15_000,
|
|
486
|
+
});
|
|
487
|
+
if (result.truncated) {
|
|
488
|
+
this.logger.warn(`Exported saved query ${saved.id} ("${saved.name}") was cut off at ${cap} rows: the mounted store cannot stream a result, so the export is bounded by maxQueryRows. The downloaded file is a prefix and says nothing about it.`);
|
|
489
|
+
}
|
|
490
|
+
return { savedQuery: saved, columns: result.columns, rows: fromRows(result.rows) };
|
|
491
|
+
}
|
|
369
492
|
listDashboards() {
|
|
370
493
|
return this.workspace ? this.workspace.listDashboards() : Promise.resolve([]);
|
|
371
494
|
}
|
|
@@ -608,7 +731,7 @@ let CatalogService = class CatalogService {
|
|
|
608
731
|
}
|
|
609
732
|
};
|
|
610
733
|
exports.CatalogService = CatalogService;
|
|
611
|
-
exports.CatalogService = CatalogService = __decorate([
|
|
734
|
+
exports.CatalogService = CatalogService = CatalogService_1 = __decorate([
|
|
612
735
|
(0, common_1.Injectable)(),
|
|
613
736
|
__param(1, (0, common_1.Inject)(catalog_store_1.CATALOG_STORE)),
|
|
614
737
|
__param(2, (0, common_1.Inject)(catalog_options_1.CATALOG_OPTIONS)),
|
package/dist/catalog.store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import type { CatalogFilterOperator, CatalogResolvedFilter } from './catalog.filters';
|
|
2
3
|
import type { CatalogObjectQuery, CatalogObjectTypeDef } from './catalog.types';
|
|
3
4
|
/**
|
|
4
5
|
* Where the objects actually live.
|
|
@@ -149,14 +150,74 @@ export interface CatalogStoreCapabilities {
|
|
|
149
150
|
* into a boot failure.
|
|
150
151
|
*/
|
|
151
152
|
export declare function isCatalogStoreCapabilities(value: unknown): value is CatalogStoreCapabilities;
|
|
152
|
-
|
|
153
|
+
/**
|
|
154
|
+
* What a store is asked for, once the service has vetted it.
|
|
155
|
+
*
|
|
156
|
+
* `Omit<..., 'filters'>` and not a plain extension, and the omission is the
|
|
157
|
+
* point: `CatalogObjectQuery.filters` is the caller's raw
|
|
158
|
+
* `property:operator:value` text, and a store must never be handed one. What
|
|
159
|
+
* arrives here instead is {@link CatalogResolvedFilter}, whose property is the
|
|
160
|
+
* type's own definition — so the column a predicate is built from came off the
|
|
161
|
+
* type rather than off the request, and the type system says so rather than a
|
|
162
|
+
* comment. `sort` is a bare string only because every store already re-matches it
|
|
163
|
+
* against the type before using it; a filter carries more than a name, so
|
|
164
|
+
* resolving it once in the service is both cheaper and harder to get wrong.
|
|
165
|
+
*/
|
|
166
|
+
export interface CatalogReadQuery extends Omit<CatalogObjectQuery, 'filters'> {
|
|
153
167
|
/** Read as of a specific snapshot. Ignored when `timeTravel` is false. */
|
|
154
168
|
snapshot?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Every one of these must be applied. A store that cannot apply one must not
|
|
171
|
+
* silently return the rows it would have returned anyway — declare the
|
|
172
|
+
* operators it can honour (see {@link CatalogFilteringReadStore}) and the
|
|
173
|
+
* service will refuse the read instead.
|
|
174
|
+
*/
|
|
175
|
+
filters?: CatalogResolvedFilter[];
|
|
155
176
|
}
|
|
156
177
|
export interface CatalogReadResult {
|
|
157
178
|
rows: Array<Record<string, unknown>>;
|
|
158
179
|
total: number;
|
|
180
|
+
/**
|
|
181
|
+
* Which snapshot these rows came from, and whether it is the one being served.
|
|
182
|
+
*
|
|
183
|
+
* Answered by the store because the store is what resolved it: a read that was
|
|
184
|
+
* given no snapshot falls back to the pointer, so only the store knows which id
|
|
185
|
+
* the rows actually carry. Reporting it costs nothing — every store that keeps
|
|
186
|
+
* history has already read both values by the time it builds the query — and it
|
|
187
|
+
* is what lets a screen say "this is not the current load" on the strength of
|
|
188
|
+
* what was read rather than of what it thinks it asked for.
|
|
189
|
+
*
|
|
190
|
+
* Absent from a store that keeps no history, which is the honest answer there:
|
|
191
|
+
* the rows are the current state and there is no other state to be reading.
|
|
192
|
+
*/
|
|
193
|
+
snapshot?: {
|
|
194
|
+
id: string;
|
|
195
|
+
current: boolean;
|
|
196
|
+
};
|
|
159
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A store that applies {@link CatalogReadQuery.filters}.
|
|
200
|
+
*
|
|
201
|
+
* Declared, never assumed, and the reason is the same one the capability object
|
|
202
|
+
* one file up gives for every field on it: a store that ignores a filter answers
|
|
203
|
+
* with more rows than were asked for, and there is nothing about that answer to
|
|
204
|
+
* distinguish it from a filter that genuinely matched everything. So a store says
|
|
205
|
+
* which operators it can push into its predicate, the service offers exactly
|
|
206
|
+
* those to the screen, and a filter naming anything else is refused rather than
|
|
207
|
+
* quietly dropped.
|
|
208
|
+
*
|
|
209
|
+
* A guard rather than a field on `CatalogStoreCapabilities`, deliberately: the
|
|
210
|
+
* capability object is intersected by the fan-out through an exhaustiveness check
|
|
211
|
+
* that fails to compile when a field is added and not composed, and this is not a
|
|
212
|
+
* property that composes the way those do — a fan-out reads through its primary,
|
|
213
|
+
* so what its primary can filter is what it can filter. Asking the object it
|
|
214
|
+
* holds is the check that stays true when that changes.
|
|
215
|
+
*/
|
|
216
|
+
export interface CatalogFilteringReadStore extends CatalogReadStore {
|
|
217
|
+
/** Which operators this store can apply. A subset of `CATALOG_FILTER_OPERATORS`. */
|
|
218
|
+
readonly objectFilterOperators: readonly CatalogFilterOperator[];
|
|
219
|
+
}
|
|
220
|
+
export declare function supportsObjectFilters(store: unknown): store is CatalogFilteringReadStore;
|
|
160
221
|
/** The minimum a store must do: return rows of a catalogued type. */
|
|
161
222
|
export interface CatalogReadStore {
|
|
162
223
|
readonly capabilities: CatalogStoreCapabilities;
|
|
@@ -346,30 +407,31 @@ export declare const CATALOG_RESERVED_COLUMNS: readonly ["_snapshot_id", "_princ
|
|
|
346
407
|
export type CatalogReservedColumn = (typeof CATALOG_RESERVED_COLUMNS)[number];
|
|
347
408
|
export declare function isReservedColumn(column: string): boolean;
|
|
348
409
|
/**
|
|
349
|
-
*
|
|
410
|
+
* The whole naming rule, which used to be written out here.
|
|
350
411
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Refuse a name that cannot be a SQL identifier.
|
|
412
|
+
* `isSafeIdentifier` and friends, the {@link physicalColumn} cleaning and the
|
|
413
|
+
* {@link outputAlias} it feeds all moved to `catalog.identifiers.ts` — a file
|
|
414
|
+
* that imports nothing — and are re-exported here so that every caller that
|
|
415
|
+
* reached them from this module still does.
|
|
416
|
+
*
|
|
417
|
+
* The move was not tidying. This file imports `BadRequestException` at module
|
|
418
|
+
* scope, so a browser importing a *value* from it would pull NestJS into its
|
|
419
|
+
* bundle, and a console proposing to replicate a table has to be able to ask,
|
|
420
|
+
* before it draws anything, whether the source's column spellings could be
|
|
421
|
+
* published as property names. The question a publisher is refused on is
|
|
422
|
+
* `isSafeIdentifier(physicalColumn(name))`, so both halves had to become
|
|
423
|
+
* reachable from `/client`, answered by the same two functions the DDL runs
|
|
424
|
+
* rather than by a copy of either. See the docblock on `catalog.identifiers.ts`.
|
|
366
425
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
426
|
+
* A bare `export … from` and not `import` + `export`, which is safe here for a
|
|
427
|
+
* reason worth writing down: nothing left in this file *calls* any of them.
|
|
428
|
+
* A re-export forwards a name without binding it locally, and while `outputAlias`
|
|
429
|
+
* still lived here — one line, calling `isSafeIdentifier` — that difference was
|
|
430
|
+
* a `ReferenceError` at the first read of any type, compiled and shipped by a
|
|
431
|
+
* rebase that had nothing to conflict on. Adding a caller here means turning
|
|
432
|
+
* this back into an import.
|
|
371
433
|
*/
|
|
372
|
-
export
|
|
434
|
+
export { assertSafeIdentifier, isSafeIdentifier, outputAlias, physicalColumn, UnsafeIdentifierError, } from './catalog.identifiers';
|
|
373
435
|
/** One property, and the column it cannot have. */
|
|
374
436
|
export interface CatalogColumnCollision {
|
|
375
437
|
/** `reserved` — it lands on a store column. `shared` — two properties collide. */
|
package/dist/catalog.store.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CATALOG_STORE = exports.CatalogColumnCollisionError = exports.UnsafeIdentifierError = exports.CATALOG_RESERVED_COLUMNS = exports.CATALOG_SNAPSHOT_MODES = void 0;
|
|
3
|
+
exports.CATALOG_STORE = exports.CatalogColumnCollisionError = exports.UnsafeIdentifierError = exports.physicalColumn = exports.outputAlias = exports.isSafeIdentifier = exports.assertSafeIdentifier = exports.CATALOG_RESERVED_COLUMNS = exports.CATALOG_SNAPSHOT_MODES = void 0;
|
|
4
4
|
exports.isCatalogStoreCapabilities = isCatalogStoreCapabilities;
|
|
5
|
+
exports.supportsObjectFilters = supportsObjectFilters;
|
|
5
6
|
exports.isReservedColumn = isReservedColumn;
|
|
6
|
-
exports.isSafeIdentifier = isSafeIdentifier;
|
|
7
|
-
exports.assertSafeIdentifier = assertSafeIdentifier;
|
|
8
7
|
exports.findColumnCollisions = findColumnCollisions;
|
|
9
8
|
exports.assertNoColumnCollisions = assertNoColumnCollisions;
|
|
10
9
|
exports.isWriteStore = isWriteStore;
|
|
@@ -49,6 +48,11 @@ function isCatalogStoreCapabilities(value) {
|
|
|
49
48
|
}
|
|
50
49
|
return true;
|
|
51
50
|
}
|
|
51
|
+
function supportsObjectFilters(store) {
|
|
52
|
+
return (typeof store === 'object' &&
|
|
53
|
+
store !== null &&
|
|
54
|
+
Array.isArray(Reflect.get(store, 'objectFilterOperators')));
|
|
55
|
+
}
|
|
52
56
|
/**
|
|
53
57
|
* The columns a snapshot-emulating store adds to every object table.
|
|
54
58
|
*
|
|
@@ -75,72 +79,36 @@ function isReservedColumn(column) {
|
|
|
75
79
|
return exports.CATALOG_RESERVED_COLUMNS.some((reserved) => reserved === column.toLowerCase());
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* a store here targets refuses a name that short, and because the number is
|
|
102
|
-
* quoted in the refusal below: a per-store limit would mean a publisher being
|
|
103
|
-
* told a different rule depending on what is mounted, for a name the catalog
|
|
104
|
-
* would then be unable to promise anything about across a fan-out.
|
|
105
|
-
*
|
|
106
|
-
* Not exported. A `RegExp` is mutable and shared state, and the two questions
|
|
107
|
-
* anyone has of it — "may I?" and "why not?" — are {@link isSafeIdentifier} and
|
|
108
|
-
* {@link UnsafeIdentifierError}.
|
|
109
|
-
*/
|
|
110
|
-
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
111
|
-
/**
|
|
112
|
-
* Why a name cannot be written into SQL, in the words a publisher is given.
|
|
113
|
-
*
|
|
114
|
-
* One class for the whole ecosystem rather than one per adapter, so
|
|
115
|
-
* `instanceof` is a usable question across packages. The publish-time check in
|
|
116
|
-
* the pipeline package catches this to tell "that name cannot be an identifier"
|
|
117
|
-
* from "something else failed inside the store", and with a class per adapter
|
|
118
|
-
* that check would re-throw the moment the mounted store was not the one it
|
|
119
|
-
* imported — turning a 400 that names the property into a 500 that names
|
|
120
|
-
* nothing.
|
|
121
|
-
*/
|
|
122
|
-
class UnsafeIdentifierError extends Error {
|
|
123
|
-
constructor(value) {
|
|
124
|
-
super(`Refusing to use "${value}" as a SQL identifier: letters, digits and underscore only, starting with a letter or underscore, 63 characters max.`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
exports.UnsafeIdentifierError = UnsafeIdentifierError;
|
|
128
|
-
/** Whether a name can be written into SQL as it stands. */
|
|
129
|
-
function isSafeIdentifier(value) {
|
|
130
|
-
return SAFE_IDENTIFIER.test(value);
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Refuse a name that cannot be a SQL identifier.
|
|
134
|
-
*
|
|
135
|
-
* Throws rather than answering, because the caller's next line writes the value
|
|
136
|
-
* into a statement: a boolean that can be ignored is a boolean that eventually
|
|
137
|
-
* is. {@link isSafeIdentifier} is there for the callers that are asking rather
|
|
138
|
-
* than about to build.
|
|
82
|
+
* The whole naming rule, which used to be written out here.
|
|
83
|
+
*
|
|
84
|
+
* `isSafeIdentifier` and friends, the {@link physicalColumn} cleaning and the
|
|
85
|
+
* {@link outputAlias} it feeds all moved to `catalog.identifiers.ts` — a file
|
|
86
|
+
* that imports nothing — and are re-exported here so that every caller that
|
|
87
|
+
* reached them from this module still does.
|
|
88
|
+
*
|
|
89
|
+
* The move was not tidying. This file imports `BadRequestException` at module
|
|
90
|
+
* scope, so a browser importing a *value* from it would pull NestJS into its
|
|
91
|
+
* bundle, and a console proposing to replicate a table has to be able to ask,
|
|
92
|
+
* before it draws anything, whether the source's column spellings could be
|
|
93
|
+
* published as property names. The question a publisher is refused on is
|
|
94
|
+
* `isSafeIdentifier(physicalColumn(name))`, so both halves had to become
|
|
95
|
+
* reachable from `/client`, answered by the same two functions the DDL runs
|
|
96
|
+
* rather than by a copy of either. See the docblock on `catalog.identifiers.ts`.
|
|
97
|
+
*
|
|
98
|
+
* A bare `export … from` and not `import` + `export`, which is safe here for a
|
|
99
|
+
* reason worth writing down: nothing left in this file *calls* any of them.
|
|
100
|
+
* A re-export forwards a name without binding it locally, and while `outputAlias`
|
|
101
|
+
* still lived here — one line, calling `isSafeIdentifier` — that difference was
|
|
102
|
+
* a `ReferenceError` at the first read of any type, compiled and shipped by a
|
|
103
|
+
* rebase that had nothing to conflict on. Adding a caller here means turning
|
|
104
|
+
* this back into an import.
|
|
139
105
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
106
|
+
var catalog_identifiers_1 = require("./catalog.identifiers");
|
|
107
|
+
Object.defineProperty(exports, "assertSafeIdentifier", { enumerable: true, get: function () { return catalog_identifiers_1.assertSafeIdentifier; } });
|
|
108
|
+
Object.defineProperty(exports, "isSafeIdentifier", { enumerable: true, get: function () { return catalog_identifiers_1.isSafeIdentifier; } });
|
|
109
|
+
Object.defineProperty(exports, "outputAlias", { enumerable: true, get: function () { return catalog_identifiers_1.outputAlias; } });
|
|
110
|
+
Object.defineProperty(exports, "physicalColumn", { enumerable: true, get: function () { return catalog_identifiers_1.physicalColumn; } });
|
|
111
|
+
Object.defineProperty(exports, "UnsafeIdentifierError", { enumerable: true, get: function () { return catalog_identifiers_1.UnsafeIdentifierError; } });
|
|
144
112
|
/**
|
|
145
113
|
* Every way a type's properties would fight over a physical column.
|
|
146
114
|
*
|
package/dist/catalog.types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* need a migration and never need an engineer. That boundary is what makes
|
|
16
16
|
* it safe to hand the editor to a non-engineer.
|
|
17
17
|
*/
|
|
18
|
+
import type { CatalogFilterOperator } from './catalog.filters';
|
|
18
19
|
export type ScalarType = 'string' | 'number' | 'boolean' | 'date' | 'json' | 'uuid' | 'unknown';
|
|
19
20
|
export type RelationKind = '1:1' | '1:m' | 'm:1' | 'm:n';
|
|
20
21
|
/** A single scalar field on an object type. */
|
|
@@ -264,6 +265,16 @@ export interface CatalogObjectQuery {
|
|
|
264
265
|
search?: string;
|
|
265
266
|
sort?: string;
|
|
266
267
|
dir?: 'asc' | 'desc';
|
|
268
|
+
/**
|
|
269
|
+
* Column filters, as they arrived: `property:operator:value`, one string each.
|
|
270
|
+
*
|
|
271
|
+
* Unvalidated, exactly like `sort` and `search` beside it — these are what a
|
|
272
|
+
* caller typed. `CatalogService.readObjects` resolves them against the type
|
|
273
|
+
* before any store sees them, and refuses the read if any of them cannot be
|
|
274
|
+
* honoured. See `catalog.filters.ts`, which owns both halves of that rule and
|
|
275
|
+
* is also what a console derives its controls from.
|
|
276
|
+
*/
|
|
277
|
+
filters?: string[];
|
|
267
278
|
}
|
|
268
279
|
export interface CatalogObjectPage {
|
|
269
280
|
type: string;
|
|
@@ -284,6 +295,59 @@ export interface CatalogObjectPage {
|
|
|
284
295
|
type: ScalarType;
|
|
285
296
|
classification?: string;
|
|
286
297
|
unit?: string;
|
|
298
|
+
/**
|
|
299
|
+
* How the source spells this column, when it is not how the property is
|
|
300
|
+
* named.
|
|
301
|
+
*
|
|
302
|
+
* Lineage, and only lineage — nothing reads a record through it. A reader
|
|
303
|
+
* recognises the source spelling because it is what is on their spreadsheet,
|
|
304
|
+
* and a filter has to be built from the property name, so both are here and a
|
|
305
|
+
* screen can show one and send the other.
|
|
306
|
+
*
|
|
307
|
+
* The two differ less often than they used to. A property may now be named
|
|
308
|
+
* `Asset Id` outright: a store cleans the name to reach its column and
|
|
309
|
+
* aliases its view to the cleaned form, so a name is no longer required to be
|
|
310
|
+
* a SQL identifier. Publishers were previously told to send `{ name:
|
|
311
|
+
* 'Asset_Id', columnName: 'Asset Id' }`, which is where most of the divergent
|
|
312
|
+
* pairs in an older catalog come from — and it was a costly instruction,
|
|
313
|
+
* because a load matches records to properties by NAME, so those columns
|
|
314
|
+
* loaded NULL. The two still differ whenever a publisher chooses different
|
|
315
|
+
* names for its own reasons, which is why the field stays.
|
|
316
|
+
*
|
|
317
|
+
* Optional: a page served by a version of this library that predates the
|
|
318
|
+
* field simply does not say, and a screen falls back to the property name.
|
|
319
|
+
*/
|
|
320
|
+
columnName?: string;
|
|
321
|
+
/**
|
|
322
|
+
* What this column may be filtered with, here and now.
|
|
323
|
+
*
|
|
324
|
+
* Derived from the column by `filterOperatorsFor` and then narrowed to what
|
|
325
|
+
* the mounted store can actually apply, so the list is the server's answer
|
|
326
|
+
* rather than the screen's guess. **Empty means not filterable** — a
|
|
327
|
+
* classified column, a blob, or a store that does not filter at all.
|
|
328
|
+
*
|
|
329
|
+
* Optional, and absent is not the same as empty: a server older than this
|
|
330
|
+
* field has not been asked. A screen must read absent the pessimistic way and
|
|
331
|
+
* offer nothing, because offering a control the server will refuse is worse
|
|
332
|
+
* than offering none.
|
|
333
|
+
*/
|
|
334
|
+
filterOperators?: CatalogFilterOperator[];
|
|
287
335
|
}>;
|
|
288
336
|
rows: Array<Record<string, unknown>>;
|
|
337
|
+
/**
|
|
338
|
+
* Which load these rows came from, when the store keeps history.
|
|
339
|
+
*
|
|
340
|
+
* Reported by the store as part of the read rather than looked up separately,
|
|
341
|
+
* so it costs nothing and — more importantly — it describes the snapshot that
|
|
342
|
+
* was actually read rather than the one the caller believes it asked for. A
|
|
343
|
+
* screen that drew its "you are looking at an old load" banner from its own
|
|
344
|
+
* state would be trusting the wrong end of the request.
|
|
345
|
+
*
|
|
346
|
+
* Absent when the store keeps no snapshots at all.
|
|
347
|
+
*/
|
|
348
|
+
snapshot?: {
|
|
349
|
+
id: string;
|
|
350
|
+
/** False means these rows are NOT what a reader gets by default. */
|
|
351
|
+
current: boolean;
|
|
352
|
+
};
|
|
289
353
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -15,6 +15,54 @@ export { CATALOG_REVISION_LIMIT } from './catalog.workspace';
|
|
|
15
15
|
export type { CatalogQueryRelation, CatalogQueryRequest, CatalogQueryResult, } from './catalog.query';
|
|
16
16
|
export type { CatalogSearchField, CatalogSearchHit, CatalogSearchKind, CatalogSearchRank, CatalogSearchResult, } from './search.types';
|
|
17
17
|
export type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogPropertyDef, CatalogRelationDef, CatalogSnapshot, RelationKind, ScalarType, } from './catalog.types';
|
|
18
|
+
/**
|
|
19
|
+
* The filter rule, shipped to the browser deliberately — the same exception, for
|
|
20
|
+
* the same reason, that `validateWorkflow` further down is.
|
|
21
|
+
*
|
|
22
|
+
* A console has to know which control to draw for a column, and the only way for
|
|
23
|
+
* that answer to match what the server will accept is for both to run this
|
|
24
|
+
* function. A screen with its own copy of the rules eventually lies: it offers a
|
|
25
|
+
* control the read refuses, or omits one that would have worked, and on types
|
|
26
|
+
* that are created at runtime nobody notices until a publisher adds a column. The
|
|
27
|
+
* functions are pure and import nothing.
|
|
28
|
+
*
|
|
29
|
+
* `SnapshotRef` rides along because a snapshot picker is a browser screen and
|
|
30
|
+
* `GET objects/:name/snapshots` is what fills it. The endpoints alone are not an
|
|
31
|
+
* API; the endpoints plus the response types are.
|
|
32
|
+
*/
|
|
33
|
+
export { CATALOG_FILTER_LIMIT, CATALOG_FILTER_OPERATORS, coerceFilterValue, encodeObjectFilter, filterOperatorTakesValue, filterOperatorsFor, isCatalogFilterOperator, offeredFilterOperators, parseObjectFilter, resolveObjectFilters, VALUELESS_FILTER_OPERATORS, } from './catalog.filters';
|
|
34
|
+
export type { CatalogFilterableColumn, CatalogFilterOperator, CatalogFilterResolution, CatalogObjectFilter, CatalogResolvedFilter, } from './catalog.filters';
|
|
35
|
+
export type { SnapshotRef } from './catalog.store';
|
|
36
|
+
/**
|
|
37
|
+
* How a name becomes a column, shipped to the browser deliberately.
|
|
38
|
+
*
|
|
39
|
+
* The second exception to "types only" on this entry point, and it is made for
|
|
40
|
+
* the same reason `validateWorkflow` is the first: the answer has to be the
|
|
41
|
+
* same one the server will give, and the only way to guarantee that is for both
|
|
42
|
+
* to run these functions.
|
|
43
|
+
*
|
|
44
|
+
* A console proposing to replicate a table has to be able to ask, *before* it
|
|
45
|
+
* draws anything, whether the source's own column spellings could be published
|
|
46
|
+
* as property names — because a property `name` is what the warehouse looks
|
|
47
|
+
* every field up by, `row[property.name]`, so a name the publisher then refuses
|
|
48
|
+
* has to be renamed, and a rename with nothing renaming the record's keys to
|
|
49
|
+
* match writes null into that column on every run and reports success. That is
|
|
50
|
+
* not a hypothesis; it is what happened to six types in one evening.
|
|
51
|
+
*
|
|
52
|
+
* **Both halves, because the composition is the rule.** A name no longer has to
|
|
53
|
+
* be an identifier itself — the view's output alias and the read's lookup key go
|
|
54
|
+
* through {@link outputAlias} together, so `Asset Id` is a perfectly good
|
|
55
|
+
* property name and lands in `Asset_Id`. What is still refused is a name whose
|
|
56
|
+
* *cleaned* form is not an identifier: `2024 Total` cleans to `2024_Total`, and
|
|
57
|
+
* no store will quote a name starting with a digit. So the question worth asking
|
|
58
|
+
* from a browser is `isSafeIdentifier(physicalColumn(name))` — the same two
|
|
59
|
+
* calls, in the same order, that `identifierRefusal` makes at publish time.
|
|
60
|
+
* Exporting only `isSafeIdentifier` would let a console answer the stricter,
|
|
61
|
+
* obsolete question and refuse a graph the server would have accepted.
|
|
62
|
+
*
|
|
63
|
+
* Safe to export because `catalog.identifiers.ts` imports nothing at all.
|
|
64
|
+
*/
|
|
65
|
+
export { isSafeIdentifier, outputAlias, physicalColumn, UnsafeIdentifierError, } from './catalog.identifiers';
|
|
18
66
|
/** What a tier-0 edit to a type may change. */
|
|
19
67
|
export interface TypePatch {
|
|
20
68
|
displayName?: string;
|
|
@@ -39,6 +87,26 @@ export interface ObjectQueryParams {
|
|
|
39
87
|
search?: string;
|
|
40
88
|
sort?: string;
|
|
41
89
|
dir?: 'asc' | 'desc';
|
|
90
|
+
/**
|
|
91
|
+
* `property:operator:value`, one entry per filter, ANDed by the server.
|
|
92
|
+
*
|
|
93
|
+
* Named `filter` rather than `filters` because that is the query parameter the
|
|
94
|
+
* route reads, and this object is handed to a transport that serialises it
|
|
95
|
+
* verbatim — a name that disagreed with the route would be a filter that is
|
|
96
|
+
* sent, ignored, and reported by the screen as applied.
|
|
97
|
+
*
|
|
98
|
+
* Build entries with `encodeObjectFilter` rather than by hand: it is what the
|
|
99
|
+
* server parses with, and the two colons are load-bearing.
|
|
100
|
+
*/
|
|
101
|
+
filter?: string[];
|
|
102
|
+
/**
|
|
103
|
+
* Read the type as of an earlier load. Omit for the current one, which is what
|
|
104
|
+
* every reader must get by default.
|
|
105
|
+
*
|
|
106
|
+
* Ids come from `GET objects/:name/snapshots`. A store that keeps no history
|
|
107
|
+
* refuses this rather than answering with current state.
|
|
108
|
+
*/
|
|
109
|
+
snapshot?: string;
|
|
42
110
|
}
|
|
43
111
|
/**
|
|
44
112
|
* Builds the paths the catalog controller serves, relative to wherever it was
|
|
@@ -85,8 +153,8 @@ export declare const catalogRoutes: {
|
|
|
85
153
|
readonly traces: () => string;
|
|
86
154
|
readonly trace: (id: string) => string;
|
|
87
155
|
};
|
|
88
|
-
export type { CatalogConnection, CatalogConnector, ConnectionCheck, CatalogTransform, CatalogWorkflow, CatalogWorkflowCapabilities, ConnectorKind, ConnectorRun, TransformLanguage, TransformResult, WorkflowEdge, WorkflowExecutionMode, WorkflowGraph, WorkflowIssueCode, WorkflowNode, WorkflowNodeKind, WorkflowNodeOutcome, WorkflowSinkNode, WorkflowSourceNode, WorkflowStageRef, WorkflowTransformNode, WorkflowValidationIssue, } from './catalog.pipeline';
|
|
89
|
-
export { CONNECTOR_KINDS, isConnectorKind, isTransformLanguage, isWorkflowEdge, isWorkflowNode, TRANSFORM_LANGUAGES, } from './catalog.pipeline';
|
|
156
|
+
export type { CatalogConnection, CatalogConnector, ConnectionCheck, CatalogTransform, CatalogWorkflow, CatalogWorkflowCapabilities, ConnectorKind, ConnectorRun, TransformLanguage, TransformResult, CallableWorkflowRef, WorkflowCallEnvelope, WorkflowCallNode, WorkflowCallOutput, WorkflowEdge, WorkflowExecutionMode, WorkflowGraph, WorkflowIssueCode, WorkflowNode, WorkflowNodeKind, WorkflowNodeOutcome, WorkflowSinkNode, WorkflowSourceNode, WorkflowStageRef, WorkflowTransformNode, WorkflowValidationIssue, } from './catalog.pipeline';
|
|
157
|
+
export { CONNECTOR_KINDS, isConnectorKind, isTransformLanguage, isWorkflowEdge, isWorkflowNode, TRANSFORM_LANGUAGES, readWorkflowCallOutput, WORKFLOW_CALL_CONTRACT, } from './catalog.pipeline';
|
|
90
158
|
/**
|
|
91
159
|
* The workflow validator, shipped to the browser deliberately.
|
|
92
160
|
*
|