@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.
@@ -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
- const { rows, total } = await this.store.read(type, fields, {
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)),
@@ -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
- export interface CatalogReadQuery extends CatalogObjectQuery {
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
- * Why a name cannot be written into SQL, in the words a publisher is given.
410
+ * The whole naming rule, which used to be written out here.
350
411
  *
351
- * One class for the whole ecosystem rather than one per adapter, so
352
- * `instanceof` is a usable question across packages. The publish-time check in
353
- * the pipeline package catches this to tell "that name cannot be an identifier"
354
- * from "something else failed inside the store", and with a class per adapter
355
- * that check would re-throw the moment the mounted store was not the one it
356
- * imported — turning a 400 that names the property into a 500 that names
357
- * nothing.
358
- */
359
- export declare class UnsafeIdentifierError extends Error {
360
- constructor(value: string);
361
- }
362
- /** Whether a name can be written into SQL as it stands. */
363
- export declare function isSafeIdentifier(value: string): boolean;
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
- * Throws rather than answering, because the caller's next line writes the value
368
- * into a statement: a boolean that can be ignored is a boolean that eventually
369
- * is. {@link isSafeIdentifier} is there for the callers that are asking rather
370
- * than about to build.
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 declare function assertSafeIdentifier(value: string): void;
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. */
@@ -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
- * What a name has to look like before a store will write it into SQL.
79
- *
80
- * Identifiers are *rejected*, never escaped and never sanitised. Every table
81
- * and column name a store emits arrives from another application over HTTP and
82
- * ends up in DDL and in SELECT lists, where no placeholder can stand in for it,
83
- * so anything outside this character set never becomes SQL at all.
84
- *
85
- * Here, beside {@link CATALOG_RESERVED_COLUMNS}, for the same reason: it is
86
- * part of what the catalog promises a *publisher*. Refuse a property name and
87
- * the sentence explaining why is the only statement of the rule most people
88
- * will ever read, so it belongs to the contract rather than to whichever
89
- * adapter happens to be mounted.
90
- *
91
- * And for one more reason. It used to be two copies — `store-mikro-orm` and
92
- * `store-clickhouse` each carried this pattern and this sentence, byte for
93
- * byte — and the publish-time refusal in the pipeline package borrowed the
94
- * MySQL one so that publish-time and DDL-time could not disagree about the
95
- * character set, the length or the wording. That bought the guarantee for a
96
- * MySQL deployment and left a ClickHouse-only one trusting two files to be
97
- * edited together. One definition is the guarantee; two identical ones are a
98
- * habit.
99
- *
100
- * 63 characters because it is under MySQL's 64-character ceiling and no engine
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
- function assertSafeIdentifier(value) {
141
- if (!isSafeIdentifier(value))
142
- throw new UnsafeIdentifierError(value);
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
  *
@@ -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
  *