@memberjunction/metadata-sync 6.2.0-edge.5 → 6.2.0-edge.7
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/README.md +151 -2
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/RecordProcessor.d.ts +16 -0
- package/dist/lib/RecordProcessor.d.ts.map +1 -1
- package/dist/lib/RecordProcessor.js +51 -4
- package/dist/lib/RecordProcessor.js.map +1 -1
- package/dist/lib/collection-promotion.d.ts +89 -0
- package/dist/lib/collection-promotion.d.ts.map +1 -0
- package/dist/lib/collection-promotion.js +154 -0
- package/dist/lib/collection-promotion.js.map +1 -0
- package/dist/lib/deletion-auditor.d.ts.map +1 -1
- package/dist/lib/deletion-auditor.js +1 -1
- package/dist/lib/deletion-auditor.js.map +1 -1
- package/dist/lib/deletion-prescan.d.ts +26 -0
- package/dist/lib/deletion-prescan.d.ts.map +1 -0
- package/dist/lib/deletion-prescan.js +56 -0
- package/dist/lib/deletion-prescan.js.map +1 -0
- package/dist/lib/env-sql-redactor.d.ts +1 -1
- package/dist/lib/env-sql-redactor.d.ts.map +1 -1
- package/dist/lib/env-sql-redactor.js +7 -72
- package/dist/lib/env-sql-redactor.js.map +1 -1
- package/dist/lib/lookup-sql.d.ts +109 -0
- package/dist/lib/lookup-sql.d.ts.map +1 -0
- package/dist/lib/lookup-sql.js +243 -0
- package/dist/lib/lookup-sql.js.map +1 -0
- package/dist/lib/natural-key-preflight.d.ts +86 -0
- package/dist/lib/natural-key-preflight.d.ts.map +1 -0
- package/dist/lib/natural-key-preflight.js +174 -0
- package/dist/lib/natural-key-preflight.js.map +1 -0
- package/dist/lib/record-dependency-analyzer.d.ts +14 -3
- package/dist/lib/record-dependency-analyzer.d.ts.map +1 -1
- package/dist/lib/record-dependency-analyzer.js +59 -72
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/lib/schema-placeholders.d.ts +29 -0
- package/dist/lib/schema-placeholders.d.ts.map +1 -0
- package/dist/lib/schema-placeholders.js +72 -0
- package/dist/lib/schema-placeholders.js.map +1 -0
- package/dist/lib/sql-log-filters.d.ts +31 -0
- package/dist/lib/sql-log-filters.d.ts.map +1 -0
- package/dist/lib/sql-log-filters.js +41 -0
- package/dist/lib/sql-log-filters.js.map +1 -0
- package/dist/lib/sql-logger.d.ts +9 -1
- package/dist/lib/sql-logger.d.ts.map +1 -1
- package/dist/lib/sql-logger.js +12 -7
- package/dist/lib/sql-logger.js.map +1 -1
- package/dist/lib/sql-set-line.d.ts +26 -0
- package/dist/lib/sql-set-line.d.ts.map +1 -0
- package/dist/lib/sql-set-line.js +84 -0
- package/dist/lib/sql-set-line.js.map +1 -0
- package/dist/lib/sync-engine.d.ts +14 -2
- package/dist/lib/sync-engine.d.ts.map +1 -1
- package/dist/lib/sync-engine.js +68 -9
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/PullService.d.ts +2 -0
- package/dist/services/PullService.d.ts.map +1 -1
- package/dist/services/PullService.js +20 -2
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.d.ts +42 -0
- package/dist/services/PushService.d.ts.map +1 -1
- package/dist/services/PushService.js +180 -58
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +31 -0
- package/dist/services/ValidationService.d.ts.map +1 -1
- package/dist/services/ValidationService.js +135 -9
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/services/WatchService.d.ts +2 -0
- package/dist/services/WatchService.d.ts.map +1 -1
- package/dist/services/WatchService.js +19 -2
- package/dist/services/WatchService.js.map +1 -1
- package/package.json +15 -15
|
@@ -11,15 +11,12 @@
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
13
13
|
import { EnvDisplayMask } from './env-display.js';
|
|
14
|
-
|
|
15
|
-
const ANY_SUFFIX = '';
|
|
16
|
-
/** The 12-hex save-call hash of a variable suffix in a statement (`_<hash>` or `_<hash>_<n>`). */
|
|
17
|
-
const SAVE_CALL_SUFFIX = /_([0-9a-fA-F]{12})(?![0-9a-fA-F])/g;
|
|
14
|
+
import { ANY_SAVE_CALL_SUFFIX, OwningLevel, SaveCallHashesIn, SetLinePattern, SqlLiteralsOf } from './sql-set-line.js';
|
|
18
15
|
/** Replaces registered SET lines and gives names-only warnings for one push. */
|
|
19
16
|
export class EnvSqlRedactor {
|
|
20
17
|
/** `variables` is read when `Warnings` runs, so a set that grows during the run can be passed. */
|
|
21
18
|
constructor(variables, env = process.env) {
|
|
22
|
-
/** Registrations by save-call hash; `
|
|
19
|
+
/** Registrations by save-call hash; `ANY_SAVE_CALL_SUFFIX` holds those registered without a hash. */
|
|
23
20
|
this.fieldsByHash = new Map();
|
|
24
21
|
this.primaryKeys = new Map();
|
|
25
22
|
this.transformFailed = false;
|
|
@@ -48,11 +45,11 @@ export class EnvSqlRedactor {
|
|
|
48
45
|
* A registration stays for the whole push.
|
|
49
46
|
*/
|
|
50
47
|
Register(entityName, fieldName, codeName, value, variableNames, saveCallHash) {
|
|
51
|
-
const literals =
|
|
48
|
+
const literals = SqlLiteralsOf(value);
|
|
52
49
|
if (literals.length === 0) {
|
|
53
50
|
return;
|
|
54
51
|
}
|
|
55
|
-
const hash = saveCallHash?.toLowerCase() ??
|
|
52
|
+
const hash = saveCallHash?.toLowerCase() ?? ANY_SAVE_CALL_SUFFIX;
|
|
56
53
|
const fields = this.fieldsForHash(hash);
|
|
57
54
|
const key = [entityName, fieldName, codeName, ...literals].join('\u0000');
|
|
58
55
|
if (fields.has(key)) {
|
|
@@ -62,7 +59,7 @@ export class EnvSqlRedactor {
|
|
|
62
59
|
EntityName: entityName,
|
|
63
60
|
FieldName: fieldName,
|
|
64
61
|
Sources: sourcesOf(variableNames),
|
|
65
|
-
Pattern:
|
|
62
|
+
Pattern: SetLinePattern(codeName, hash, literals),
|
|
66
63
|
Matched: false,
|
|
67
64
|
});
|
|
68
65
|
}
|
|
@@ -96,7 +93,7 @@ export class EnvSqlRedactor {
|
|
|
96
93
|
this.RegisterPrimaryKey(entity.EntityInfo.Name, fieldName, variables);
|
|
97
94
|
continue;
|
|
98
95
|
}
|
|
99
|
-
const level =
|
|
96
|
+
const level = OwningLevel(entity, fieldName);
|
|
100
97
|
const field = level.GetFieldByName(fieldName);
|
|
101
98
|
const levelInfo = level.EntityInfo;
|
|
102
99
|
const holdsField = fullRow || !levelInfo.spUpdateGenerated || fieldName in level.GetChangesSinceLastSave();
|
|
@@ -129,10 +126,7 @@ export class EnvSqlRedactor {
|
|
|
129
126
|
}
|
|
130
127
|
/** The registrations that can match the statement: those of each suffix in it, and those without a hash. */
|
|
131
128
|
candidateFields(sql) {
|
|
132
|
-
const hashes =
|
|
133
|
-
for (const match of sql.matchAll(SAVE_CALL_SUFFIX)) {
|
|
134
|
-
hashes.add(match[1].toLowerCase());
|
|
135
|
-
}
|
|
129
|
+
const hashes = SaveCallHashesIn(sql);
|
|
136
130
|
const candidates = [];
|
|
137
131
|
for (const hash of hashes) {
|
|
138
132
|
this.fieldsByHash.get(hash)?.forEach((field) => candidates.push(field));
|
|
@@ -186,14 +180,6 @@ export class EnvSqlRedactor {
|
|
|
186
180
|
return warnings;
|
|
187
181
|
}
|
|
188
182
|
}
|
|
189
|
-
/** The IS-A level of `entity` whose table holds the field: walks up while a parent holds it. */
|
|
190
|
-
function owningLevel(entity, fieldName) {
|
|
191
|
-
let level = entity;
|
|
192
|
-
while (level.ISAParent && level.EntityInfo.ParentEntityFieldNames?.has(fieldName)) {
|
|
193
|
-
level = level.ISAParent;
|
|
194
|
-
}
|
|
195
|
-
return level;
|
|
196
|
-
}
|
|
197
183
|
/** The log text, or null when the file cannot be read. */
|
|
198
184
|
function readLog(sqlLogPath) {
|
|
199
185
|
try {
|
|
@@ -211,57 +197,6 @@ function valueIsInText(name, text, env) {
|
|
|
211
197
|
function sourcesOf(variableNames) {
|
|
212
198
|
return [...variableNames].sort().map((name) => `@env:${name}`).join(', ');
|
|
213
199
|
}
|
|
214
|
-
/** The literal forms SQL Server writes for a value in a SET line. Empty when none can be matched. */
|
|
215
|
-
function sqlLiteralsOf(value) {
|
|
216
|
-
if (value instanceof Date) {
|
|
217
|
-
return Number.isNaN(value.getTime()) ? [] : [`'${value.toISOString()}'`];
|
|
218
|
-
}
|
|
219
|
-
if (typeof value === 'boolean') {
|
|
220
|
-
return [value ? '1' : '0'];
|
|
221
|
-
}
|
|
222
|
-
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
223
|
-
return [String(value)];
|
|
224
|
-
}
|
|
225
|
-
if (value !== null && typeof value === 'object') {
|
|
226
|
-
return quotedLiterals(JSON.stringify(value));
|
|
227
|
-
}
|
|
228
|
-
if (typeof value !== 'string') {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
return [...quotedLiterals(value), ...stringValueLiterals(value)];
|
|
232
|
-
}
|
|
233
|
-
/** The number or bit literal forms of a string value that holds a number or a boolean. */
|
|
234
|
-
function stringValueLiterals(value) {
|
|
235
|
-
const trimmed = value.trim();
|
|
236
|
-
const literals = [];
|
|
237
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
238
|
-
literals.push(trimmed);
|
|
239
|
-
}
|
|
240
|
-
const lower = trimmed.toLowerCase();
|
|
241
|
-
if (lower === 'true') {
|
|
242
|
-
literals.push('1');
|
|
243
|
-
}
|
|
244
|
-
else if (lower === 'false') {
|
|
245
|
-
literals.push('0');
|
|
246
|
-
}
|
|
247
|
-
return literals;
|
|
248
|
-
}
|
|
249
|
-
function quotedLiterals(text) {
|
|
250
|
-
const escaped = text.replace(/'/g, "''");
|
|
251
|
-
return [`N'${escaped}'`, `'${escaped}'`];
|
|
252
|
-
}
|
|
253
|
-
function escapeRegExp(text) {
|
|
254
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* One whole `SET @<codeName>_<suffix>[_n] = <literal>` line; group 1 is the indent. The suffix is
|
|
258
|
-
* the given hash, or any 12 hex when the hash is `ANY_SUFFIX`.
|
|
259
|
-
*/
|
|
260
|
-
function setLinePattern(codeName, hash, literals) {
|
|
261
|
-
const alternatives = literals.map(escapeRegExp).join('|');
|
|
262
|
-
const suffix = hash === ANY_SUFFIX ? '[0-9a-fA-F]{12}' : escapeRegExp(hash);
|
|
263
|
-
return new RegExp(`^([ \\t]*)SET @${escapeRegExp(codeName)}_${suffix}(?:_\\d+)? = (?:${alternatives})[ \\t]*$`, 'gm');
|
|
264
|
-
}
|
|
265
200
|
/**
|
|
266
201
|
* The redactor of one open SQL log, with the `SyncEngine.BeforeLookupCreateSave` hook that registers
|
|
267
202
|
* each record a `?create` lookup creates (with the full row, because a create writes every field).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-sql-redactor.js","sourceRoot":"","sources":["../../src/lib/env-sql-redactor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,OAAO,EAAE,uBAAuB,EAAE,MAAM,2CAA2C,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"env-sql-redactor.js","sourceRoot":"","sources":["../../src/lib/env-sql-redactor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Writes `@lookup` values into the push SQL log as subqueries on the natural key
|
|
3
|
+
* (#5626, #3619).
|
|
4
|
+
*
|
|
5
|
+
* A lookup resolves at push time to this database's ID. When the push is captured as a migration,
|
|
6
|
+
* that literal ID matches nothing on a database whose row has another ID (one CodeGen created, or
|
|
7
|
+
* one created before a baseline shipped), so an update is a silent no-op and an insert fails on a
|
|
8
|
+
* foreign key. For a primary-key or foreign-key field whose value came from a `@lookup`, the logged
|
|
9
|
+
* `SET @<Field>_<hash> = '<id>'` line is replaced with `SET @<Field>_<hash> = (SELECT …)` on the
|
|
10
|
+
* lookup's criteria. Nested lookups nest. The subquery names the real schema; the logger's schema
|
|
11
|
+
* placeholder rewrite runs after `TransformStatement` and turns it into a placeholder.
|
|
12
|
+
*
|
|
13
|
+
* Only SQL Server save statements have SET lines, so a PostgreSQL log keeps literal IDs.
|
|
14
|
+
* @module lookup-sql
|
|
15
|
+
*/
|
|
16
|
+
import type { BaseEntity, EntityInfo } from '@memberjunction/core';
|
|
17
|
+
import type { SyncEngine } from './sync-engine.js';
|
|
18
|
+
/** One `Field=Value` pair of a `@lookup`. A nested `@lookup` value keeps its own source. */
|
|
19
|
+
export interface LookupSqlCriterion {
|
|
20
|
+
FieldName: string;
|
|
21
|
+
/** The resolved value as text, or the source of a nested `@lookup`. */
|
|
22
|
+
Value: string | LookupSqlSource;
|
|
23
|
+
}
|
|
24
|
+
/** What a `@lookup` matched on, kept so the SQL log can match on it too. */
|
|
25
|
+
export interface LookupSqlSource {
|
|
26
|
+
/** The `@lookup:` text as written. */
|
|
27
|
+
Expression: string;
|
|
28
|
+
EntityName: string;
|
|
29
|
+
Criteria: LookupSqlCriterion[];
|
|
30
|
+
/** True when a criterion value (or a nested lookup's) came from `@env:`. */
|
|
31
|
+
EnvSourced: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** A built subquery (`Sql`), or why none could be built (`Problem`). Exactly one is set. */
|
|
34
|
+
export interface LookupSubquery {
|
|
35
|
+
Sql?: string;
|
|
36
|
+
Problem?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Looks up entity metadata by name. */
|
|
39
|
+
export type EntityInfoLookup = (entityName: string) => EntityInfo | null | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Builds `(SELECT [PK] FROM [schema].[BaseTable] WHERE …)` for a lookup source. Each criterion is
|
|
42
|
+
* matched with `=` on the entity's own table. A string criterion compares both sides lower-cased,
|
|
43
|
+
* as the push lookup does, so the replay finds the same row whatever the column's collation (a
|
|
44
|
+
* case-sensitive SQL Server column, or PostgreSQL once converted). `null` matches `IS NULL`. A criterion must be a column of the entity's
|
|
45
|
+
* own table: a virtual field or one an IS-A parent holds has no column there.
|
|
46
|
+
*/
|
|
47
|
+
export declare class LookupSubqueryBuilder {
|
|
48
|
+
private readonly entityInfo;
|
|
49
|
+
constructor(entityInfo: EntityInfoLookup);
|
|
50
|
+
Build(source: LookupSqlSource): LookupSubquery;
|
|
51
|
+
private condition;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replaces the logged SET lines of lookup-sourced fields with subqueries, for one push. Registrations
|
|
55
|
+
* are keyed by the save-call hash of the record (`GenericDatabaseProvider.SaveCallVariableHash`), so
|
|
56
|
+
* only the SET line of that record's save is rewritten. A registration stays for the whole push.
|
|
57
|
+
*/
|
|
58
|
+
export declare class LookupSqlRewriter {
|
|
59
|
+
private readonly builder;
|
|
60
|
+
private readonly linesByHash;
|
|
61
|
+
private readonly warnings;
|
|
62
|
+
constructor(builder: LookupSubqueryBuilder);
|
|
63
|
+
/**
|
|
64
|
+
* Registers one field of one save call. `value` is the resolved ID the statement holds. Returns
|
|
65
|
+
* false (and keeps a warning) when no subquery can be built, so the log keeps the literal ID.
|
|
66
|
+
*/
|
|
67
|
+
Register(entityName: string, fieldName: string, codeName: string, value: unknown, lookup: LookupSqlSource, saveCallHash: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Registers each lookup-sourced field of the save statement of `entity`. A primary key is
|
|
70
|
+
* registered at every IS-A level, because each level's save sets it; another field at the level
|
|
71
|
+
* whose table holds it. Call it right before the save, so the hash uses the key values the
|
|
72
|
+
* provider hashes. `lookups` keys are matched without regard to case.
|
|
73
|
+
*/
|
|
74
|
+
RegisterEntityLookups(entity: BaseEntity, lookups: ReadonlyMap<string, LookupSqlSource> | undefined): void;
|
|
75
|
+
/**
|
|
76
|
+
* Registers the primary key of a record that a `@lookup:…?create` made, keyed by the lookup that
|
|
77
|
+
* made it, and keeps a warning: where no row matches, the replay passes a NULL ID, so the
|
|
78
|
+
* `IF NOT EXISTS` guard inserts the row with an ID that database generates.
|
|
79
|
+
*/
|
|
80
|
+
RegisterLookupCreate(created: BaseEntity, lookup: LookupSqlSource): void;
|
|
81
|
+
/**
|
|
82
|
+
* The `SqlLoggingOptions.TransformStatement` function. It can run many times on the same text and
|
|
83
|
+
* on text that is never written. On an unexpected error it returns the text unchanged, so the log
|
|
84
|
+
* keeps the literal IDs, and keeps a warning.
|
|
85
|
+
*/
|
|
86
|
+
readonly Transform: (sql: string) => string;
|
|
87
|
+
/** The warnings of this run, each once. */
|
|
88
|
+
Warnings(): string[];
|
|
89
|
+
private registerLevelField;
|
|
90
|
+
private replaceSetLines;
|
|
91
|
+
}
|
|
92
|
+
/** The sync engine members a `LookupSqlLog` uses. */
|
|
93
|
+
type LookupSqlLogEngine = Pick<SyncEngine, 'BeforeLookupCreateSaveSource' | 'GetEntityInfo'>;
|
|
94
|
+
/**
|
|
95
|
+
* The lookup rewriter of one open SQL log, with the `SyncEngine.BeforeLookupCreateSaveSource` hook
|
|
96
|
+
* that registers each record a `?create` lookup makes.
|
|
97
|
+
*/
|
|
98
|
+
export declare class LookupSqlLog {
|
|
99
|
+
readonly Rewriter: LookupSqlRewriter;
|
|
100
|
+
private readonly engine;
|
|
101
|
+
private readonly hook;
|
|
102
|
+
constructor(engine: LookupSqlLogEngine);
|
|
103
|
+
/** Clears the engine's hook when it is still this log's hook. */
|
|
104
|
+
Close(): void;
|
|
105
|
+
}
|
|
106
|
+
/** One `TransformStatement` that runs each transform in turn. */
|
|
107
|
+
export declare function ComposeStatementTransforms(...transforms: Array<(sql: string) => string>): (sql: string) => string;
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=lookup-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup-sql.d.ts","sourceRoot":"","sources":["../../src/lib/lookup-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,UAAU,EAAmB,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,eAAe,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;AAErF;;;;;;GAMG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;gBAElC,UAAU,EAAE,gBAAgB;IAIxC,KAAK,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc;IA0B9C,OAAO,CAAC,SAAS;CAsBlB;AA2CD;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiD;IAC7E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,OAAO,EAAE,qBAAqB;IAI1C;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAwBzI;;;;;OAKG;IACH,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,SAAS,GAAG,IAAI;IAiB1G;;;;OAIG;IACH,oBAAoB,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAwBxE;;;;OAIG;IACH,QAAQ,CAAC,SAAS,GAAI,KAAK,MAAM,KAAG,MAAM,CAOxC;IAEF,2CAA2C;IAC3C,QAAQ,IAAI,MAAM,EAAE;IAIpB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,eAAe;CAYxB;AAED,qDAAqD;AACrD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,8BAA8B,GAAG,eAAe,CAAC,CAAC;AAE7F;;;GAGG;AACH,qBAAa,YAAY;IACvB,SAAgB,QAAQ,EAAE,iBAAiB,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0D;gBAEnE,MAAM,EAAE,kBAAkB;IAOtC,iEAAiE;IACjE,KAAK,IAAI,IAAI;CAKd;AAED,iEAAiE;AACjE,wBAAgB,0BAA0B,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAEjH"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
2
|
+
import { GetDialect, IsDateSQLType, IsUuidSQLType } from '@memberjunction/sql-dialect';
|
|
3
|
+
import { IsALevels, OwningLevel, SaveCallHashesIn, SetLinePattern, SqlLiteralsOf } from './sql-set-line.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds `(SELECT [PK] FROM [schema].[BaseTable] WHERE …)` for a lookup source. Each criterion is
|
|
6
|
+
* matched with `=` on the entity's own table. A string criterion compares both sides lower-cased,
|
|
7
|
+
* as the push lookup does, so the replay finds the same row whatever the column's collation (a
|
|
8
|
+
* case-sensitive SQL Server column, or PostgreSQL once converted). `null` matches `IS NULL`. A criterion must be a column of the entity's
|
|
9
|
+
* own table: a virtual field or one an IS-A parent holds has no column there.
|
|
10
|
+
*/
|
|
11
|
+
export class LookupSubqueryBuilder {
|
|
12
|
+
constructor(entityInfo) {
|
|
13
|
+
this.entityInfo = entityInfo;
|
|
14
|
+
}
|
|
15
|
+
Build(source) {
|
|
16
|
+
if (source.EnvSourced) {
|
|
17
|
+
return { Problem: 'its @lookup criteria use an @env value, which the log never holds' };
|
|
18
|
+
}
|
|
19
|
+
const info = this.entityInfo(source.EntityName);
|
|
20
|
+
if (!info) {
|
|
21
|
+
return { Problem: `entity '${source.EntityName}' was not found` };
|
|
22
|
+
}
|
|
23
|
+
if (info.PrimaryKeys.length !== 1) {
|
|
24
|
+
return { Problem: `'${source.EntityName}' does not have a single-column primary key` };
|
|
25
|
+
}
|
|
26
|
+
if (source.Criteria.length === 0) {
|
|
27
|
+
return { Problem: 'the @lookup has no criteria' };
|
|
28
|
+
}
|
|
29
|
+
const conditions = [];
|
|
30
|
+
for (const criterion of source.Criteria) {
|
|
31
|
+
const condition = this.condition(info, criterion);
|
|
32
|
+
if (condition.Problem !== undefined) {
|
|
33
|
+
return condition;
|
|
34
|
+
}
|
|
35
|
+
conditions.push(condition.Sql);
|
|
36
|
+
}
|
|
37
|
+
const key = info.FirstPrimaryKey.Name; // first-pk-ok: a single-column key was checked above; a lookup yields one value
|
|
38
|
+
return { Sql: `(SELECT [${key}] FROM [${info.SchemaName}].[${info.BaseTable}] WHERE ${conditions.join(' AND ')})` };
|
|
39
|
+
}
|
|
40
|
+
condition(info, criterion) {
|
|
41
|
+
const wanted = criterion.FieldName.trim().toLowerCase();
|
|
42
|
+
const field = info.Fields.find((candidate) => candidate.Name.toLowerCase() === wanted);
|
|
43
|
+
if (!field) {
|
|
44
|
+
return { Problem: `'${info.Name}' has no field '${criterion.FieldName}'` };
|
|
45
|
+
}
|
|
46
|
+
if (field.IsVirtual || info.ParentEntityFieldNames?.has(field.Name)) {
|
|
47
|
+
return { Problem: `${info.Name}.${field.Name} is not a column of table ${info.BaseTable}` };
|
|
48
|
+
}
|
|
49
|
+
if (typeof criterion.Value !== 'string') {
|
|
50
|
+
const nested = this.Build(criterion.Value);
|
|
51
|
+
return nested.Problem !== undefined ? nested : { Sql: `[${field.Name}] = ${nested.Sql}` };
|
|
52
|
+
}
|
|
53
|
+
if (criterion.Value.trim().toLowerCase() === 'null') {
|
|
54
|
+
return { Sql: `[${field.Name}] IS NULL` };
|
|
55
|
+
}
|
|
56
|
+
const literal = criterionLiteral(field, criterion.Value);
|
|
57
|
+
if (literal === null) {
|
|
58
|
+
return { Problem: `the value of ${info.Name}.${field.Name} cannot be written as a ${field.Type} literal` };
|
|
59
|
+
}
|
|
60
|
+
return { Sql: criterionComparison(field, `[${field.Name}]`, literal) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The SQL Server dialect: the log holds SQL Server statements, and the converter carries them to PostgreSQL. */
|
|
64
|
+
const LOG_DIALECT = GetDialect('sqlserver');
|
|
65
|
+
/**
|
|
66
|
+
* `column = literal`, lower-casing both sides for a string field, as `SyncEngine`'s push lookup
|
|
67
|
+
* does. A uuid or date field is compared as is: PostgreSQL has no `lower(uuid)`, and one failing
|
|
68
|
+
* statement aborts the whole replay.
|
|
69
|
+
*/
|
|
70
|
+
function criterionComparison(field, column, literal) {
|
|
71
|
+
const isString = field.NeedsQuotes && !IsUuidSQLType(field.Type) && !IsDateSQLType(field.Type);
|
|
72
|
+
return isString ? `${LOG_DIALECT.LowerCase(column)} = ${LOG_DIALECT.LowerCase(literal)}` : `${column} = ${literal}`;
|
|
73
|
+
}
|
|
74
|
+
/** The SQL Server literal for a criterion value, or null when the value does not fit the field type. */
|
|
75
|
+
function criterionLiteral(field, value) {
|
|
76
|
+
const escaped = value.replace(/'/g, "''");
|
|
77
|
+
if (!field.NeedsQuotes) {
|
|
78
|
+
const type = field.Type.trim().toLowerCase();
|
|
79
|
+
const lower = value.trim().toLowerCase();
|
|
80
|
+
if (type === 'bit' || type === 'boolean') {
|
|
81
|
+
if (lower === 'true' || lower === '1') {
|
|
82
|
+
return '1';
|
|
83
|
+
}
|
|
84
|
+
if (lower === 'false' || lower === '0') {
|
|
85
|
+
return '0';
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return /^-?\d+(\.\d+)?$/.test(value.trim()) ? value.trim() : null;
|
|
90
|
+
}
|
|
91
|
+
if (IsUuidSQLType(field.Type) || IsDateSQLType(field.Type)) {
|
|
92
|
+
return `'${escaped}'`;
|
|
93
|
+
}
|
|
94
|
+
return `N'${escaped}'`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Replaces the logged SET lines of lookup-sourced fields with subqueries, for one push. Registrations
|
|
98
|
+
* are keyed by the save-call hash of the record (`GenericDatabaseProvider.SaveCallVariableHash`), so
|
|
99
|
+
* only the SET line of that record's save is rewritten. A registration stays for the whole push.
|
|
100
|
+
*/
|
|
101
|
+
export class LookupSqlRewriter {
|
|
102
|
+
constructor(builder) {
|
|
103
|
+
this.linesByHash = new Map();
|
|
104
|
+
this.warnings = new Set();
|
|
105
|
+
/**
|
|
106
|
+
* The `SqlLoggingOptions.TransformStatement` function. It can run many times on the same text and
|
|
107
|
+
* on text that is never written. On an unexpected error it returns the text unchanged, so the log
|
|
108
|
+
* keeps the literal IDs, and keeps a warning.
|
|
109
|
+
*/
|
|
110
|
+
this.Transform = (sql) => {
|
|
111
|
+
try {
|
|
112
|
+
return this.replaceSetLines(sql);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
this.warnings.add('SQL log: a statement kept its literal lookup IDs because the rewrite to subqueries failed.');
|
|
116
|
+
return sql;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
this.builder = builder;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Registers one field of one save call. `value` is the resolved ID the statement holds. Returns
|
|
123
|
+
* false (and keeps a warning) when no subquery can be built, so the log keeps the literal ID.
|
|
124
|
+
*/
|
|
125
|
+
Register(entityName, fieldName, codeName, value, lookup, saveCallHash) {
|
|
126
|
+
const literals = SqlLiteralsOf(value);
|
|
127
|
+
if (literals.length === 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const subquery = this.builder.Build(lookup);
|
|
131
|
+
if (subquery.Problem !== undefined || subquery.Sql === undefined) {
|
|
132
|
+
this.warnings.add(`SQL log: ${entityName}.${fieldName} is written with this database's ID because ${subquery.Problem} (${lookup.Expression}). A database where that row has another ID will not match it.`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const hash = saveCallHash.toLowerCase();
|
|
136
|
+
let lines = this.linesByHash.get(hash);
|
|
137
|
+
if (!lines) {
|
|
138
|
+
lines = new Map();
|
|
139
|
+
this.linesByHash.set(hash, lines);
|
|
140
|
+
}
|
|
141
|
+
const key = [codeName, ...literals].join('\u0000');
|
|
142
|
+
// Case-insensitive: SQL Server returns upper-case UUIDs where a lookup can return either case
|
|
143
|
+
lines.set(key, { Pattern: SetLinePattern(codeName, hash, literals, 'gmi'), Subquery: subquery.Sql });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Registers each lookup-sourced field of the save statement of `entity`. A primary key is
|
|
148
|
+
* registered at every IS-A level, because each level's save sets it; another field at the level
|
|
149
|
+
* whose table holds it. Call it right before the save, so the hash uses the key values the
|
|
150
|
+
* provider hashes. `lookups` keys are matched without regard to case.
|
|
151
|
+
*/
|
|
152
|
+
RegisterEntityLookups(entity, lookups) {
|
|
153
|
+
if (!lookups || lookups.size === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const keyNames = new Set(entity.EntityInfo.PrimaryKeys.map((pk) => pk.Name));
|
|
157
|
+
for (const [key, lookup] of lookups) {
|
|
158
|
+
const fieldName = entity.GetFieldByName(key)?.Name;
|
|
159
|
+
if (!fieldName) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const levels = keyNames.has(fieldName) ? IsALevels(entity) : [OwningLevel(entity, fieldName)];
|
|
163
|
+
for (const level of levels) {
|
|
164
|
+
this.registerLevelField(level, fieldName, lookup);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Registers the primary key of a record that a `@lookup:…?create` made, keyed by the lookup that
|
|
170
|
+
* made it, and keeps a warning: where no row matches, the replay passes a NULL ID, so the
|
|
171
|
+
* `IF NOT EXISTS` guard inserts the row with an ID that database generates.
|
|
172
|
+
*/
|
|
173
|
+
RegisterLookupCreate(created, lookup) {
|
|
174
|
+
if (created.EntityInfo.PrimaryKeys.length !== 1) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Criteria that were lookups themselves are foreign keys the create sets: they nest too
|
|
178
|
+
const nested = new Map();
|
|
179
|
+
for (const criterion of lookup.Criteria) {
|
|
180
|
+
if (typeof criterion.Value !== 'string') {
|
|
181
|
+
nested.set(criterion.FieldName, criterion.Value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.RegisterEntityLookups(created, nested);
|
|
185
|
+
const keyName = created.EntityInfo.FirstPrimaryKey.Name; // first-pk-ok: a single-column key was checked above
|
|
186
|
+
let registered = false;
|
|
187
|
+
for (const level of IsALevels(created)) {
|
|
188
|
+
registered = this.registerLevelField(level, keyName, lookup) || registered;
|
|
189
|
+
}
|
|
190
|
+
if (registered) {
|
|
191
|
+
this.warnings.add(`SQL log: the ${created.EntityInfo.Name} row that ${lookup.Expression} created is logged keyed by that lookup. On a database with no matching row the replay passes a NULL ID, so the IF NOT EXISTS guard inserts the row with an ID that database generates.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The warnings of this run, each once. */
|
|
195
|
+
Warnings() {
|
|
196
|
+
return [...this.warnings];
|
|
197
|
+
}
|
|
198
|
+
registerLevelField(level, fieldName, lookup) {
|
|
199
|
+
const field = level.GetFieldByName(fieldName);
|
|
200
|
+
if (!field) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const levelInfo = level.EntityInfo;
|
|
204
|
+
const keyValues = level.PrimaryKey?.KeyValuePairs?.map((pair) => pair.Value) ?? [];
|
|
205
|
+
const saveCallHash = GenericDatabaseProvider.SaveCallVariableHash(levelInfo.SchemaName, levelInfo.BaseTable, keyValues);
|
|
206
|
+
return this.Register(levelInfo.Name, fieldName, field.EntityFieldInfo.CodeName, level.Get(fieldName), lookup, saveCallHash);
|
|
207
|
+
}
|
|
208
|
+
replaceSetLines(sql) {
|
|
209
|
+
if (this.linesByHash.size === 0) {
|
|
210
|
+
return sql;
|
|
211
|
+
}
|
|
212
|
+
let result = sql;
|
|
213
|
+
for (const hash of SaveCallHashesIn(sql)) {
|
|
214
|
+
for (const line of this.linesByHash.get(hash)?.values() ?? []) {
|
|
215
|
+
result = result.replace(line.Pattern, (_line, indent, variable) => `${indent}SET ${variable} = ${line.Subquery}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The lookup rewriter of one open SQL log, with the `SyncEngine.BeforeLookupCreateSaveSource` hook
|
|
223
|
+
* that registers each record a `?create` lookup makes.
|
|
224
|
+
*/
|
|
225
|
+
export class LookupSqlLog {
|
|
226
|
+
constructor(engine) {
|
|
227
|
+
this.engine = engine;
|
|
228
|
+
this.Rewriter = new LookupSqlRewriter(new LookupSubqueryBuilder((name) => engine.GetEntityInfo(name)));
|
|
229
|
+
this.hook = (created, lookup) => this.Rewriter.RegisterLookupCreate(created, lookup);
|
|
230
|
+
engine.BeforeLookupCreateSaveSource = this.hook;
|
|
231
|
+
}
|
|
232
|
+
/** Clears the engine's hook when it is still this log's hook. */
|
|
233
|
+
Close() {
|
|
234
|
+
if (this.engine.BeforeLookupCreateSaveSource === this.hook) {
|
|
235
|
+
this.engine.BeforeLookupCreateSaveSource = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** One `TransformStatement` that runs each transform in turn. */
|
|
240
|
+
export function ComposeStatementTransforms(...transforms) {
|
|
241
|
+
return (sql) => transforms.reduce((text, transform) => transform(text), sql);
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=lookup-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup-sql.js","sourceRoot":"","sources":["../../src/lib/lookup-sql.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pre-flight for a create that a unique constraint would reject (#4400).
|
|
3
|
+
*
|
|
4
|
+
* A seed record with a literal primary key that is not in the database is created with that key.
|
|
5
|
+
* When the database already has a row with the same natural key under another ID (CodeGen and
|
|
6
|
+
* other server-side code create rows with database-generated IDs), the create violates the unique
|
|
7
|
+
* constraint and the push stops on a driver error. This check runs first: for each unique key of
|
|
8
|
+
* the table, it looks the record's values up and, on a hit, reports the existing row's key so the
|
|
9
|
+
* seed can be re-keyed with `@lookup`.
|
|
10
|
+
* @module natural-key-preflight
|
|
11
|
+
*/
|
|
12
|
+
import { type BaseEntity, type DatabaseProviderBase, type EntityFieldInfo, type EntityInfo, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
13
|
+
/** One unique key of a table: its name and its columns in key order. */
|
|
14
|
+
export interface UniqueKeyColumns {
|
|
15
|
+
Name: string;
|
|
16
|
+
Columns: string[];
|
|
17
|
+
}
|
|
18
|
+
/** A row that already holds the natural key a create would add. */
|
|
19
|
+
export interface NaturalKeyConflict {
|
|
20
|
+
/** Unique constraint or index name. */
|
|
21
|
+
KeyName: string;
|
|
22
|
+
/** The entity's field names that make up the key. */
|
|
23
|
+
Fields: string[];
|
|
24
|
+
/** The primary key of the existing row. */
|
|
25
|
+
ExistingKey: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Reads unique keys and finds rows by them. */
|
|
28
|
+
export interface NaturalKeyReader {
|
|
29
|
+
/** The unique keys of the entity's table, other than the primary key. */
|
|
30
|
+
UniqueKeys(info: EntityInfo): Promise<UniqueKeyColumns[]>;
|
|
31
|
+
/** The primary key of a row whose fields equal `values`, or null when there is none. */
|
|
32
|
+
FindKey(info: EntityInfo, values: ReadonlyArray<{
|
|
33
|
+
Field: EntityFieldInfo;
|
|
34
|
+
Value: unknown;
|
|
35
|
+
}>): Promise<Record<string, unknown> | null>;
|
|
36
|
+
}
|
|
37
|
+
/** Finds the existing row, if any, that a create of `entity` would collide with on a unique key. */
|
|
38
|
+
export declare class NaturalKeyPreflight {
|
|
39
|
+
private readonly reader;
|
|
40
|
+
constructor(reader: NaturalKeyReader);
|
|
41
|
+
/**
|
|
42
|
+
* The first unique key whose values another row already holds, or null. A key is checked only
|
|
43
|
+
* when every one of its columns is a field of the entity with a non-null value and no column is
|
|
44
|
+
* part of the primary key.
|
|
45
|
+
*/
|
|
46
|
+
Check(entity: BaseEntity): Promise<NaturalKeyConflict | null>;
|
|
47
|
+
private keyValues;
|
|
48
|
+
}
|
|
49
|
+
/** Row shape of the unique-key catalog queries. */
|
|
50
|
+
interface UniqueKeyCatalogRow {
|
|
51
|
+
IndexName: string;
|
|
52
|
+
ColumnName: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reads unique keys from the database catalog (unfiltered unique indexes and unique constraints,
|
|
56
|
+
* not the primary key) and finds rows with `RunView` on the record's provider, so the lookup sees
|
|
57
|
+
* rows the push transaction wrote. Catalog reads go to the pool and are never logged. A catalog
|
|
58
|
+
* read that fails gives no keys, so the check is skipped rather than failing the push.
|
|
59
|
+
*/
|
|
60
|
+
export declare class DatabaseNaturalKeyReader implements NaturalKeyReader {
|
|
61
|
+
private readonly provider;
|
|
62
|
+
private readonly contextUser;
|
|
63
|
+
private readonly rowProvider;
|
|
64
|
+
private readonly cache;
|
|
65
|
+
/**
|
|
66
|
+
* `rowProvider` is the provider rows are found on (the record's, so the read joins its
|
|
67
|
+
* transaction); `cache` (entity name → keys) can be shared by readers that find rows on
|
|
68
|
+
* different providers.
|
|
69
|
+
*/
|
|
70
|
+
constructor(provider: DatabaseProviderBase, contextUser: UserInfo, rowProvider?: IMetadataProvider, cache?: Map<string, Promise<UniqueKeyColumns[]>>);
|
|
71
|
+
UniqueKeys(info: EntityInfo): Promise<UniqueKeyColumns[]>;
|
|
72
|
+
FindKey(info: EntityInfo, values: ReadonlyArray<{
|
|
73
|
+
Field: EntityFieldInfo;
|
|
74
|
+
Value: unknown;
|
|
75
|
+
}>): Promise<Record<string, unknown> | null>;
|
|
76
|
+
private readUniqueKeys;
|
|
77
|
+
private literal;
|
|
78
|
+
}
|
|
79
|
+
/** The catalog query listing a table's unfiltered unique keys other than the primary key, one row per column in key order. */
|
|
80
|
+
export declare function UniqueKeyCatalogSQL(platformKey: string, schemaName: string, tableName: string): string;
|
|
81
|
+
/** Catalog rows grouped into keys, in row order. */
|
|
82
|
+
export declare function GroupUniqueKeyRows(rows: ReadonlyArray<UniqueKeyCatalogRow>): UniqueKeyColumns[];
|
|
83
|
+
/** The pre-flight error for a conflict. `recordKey` is the record's key as it may be shown. */
|
|
84
|
+
export declare function NaturalKeyConflictMessage(entityName: string, recordKey: string, conflict: NaturalKeyConflict): string;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=natural-key-preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"natural-key-preflight.d.ts","sourceRoot":"","sources":["../../src/lib/natural-key-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAW,KAAK,UAAU,EAAE,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEzK,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC1D,wFAAwF;IACxF,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;CACvI;AAED,oGAAoG;AACpG,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,MAAM,EAAE,gBAAgB;IAIpC;;;;OAIG;IACG,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAoBnE,OAAO,CAAC,SAAS;CAkBlB;AASD,mDAAmD;AACnD,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgC;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2C;IAEjE;;;;OAIG;gBAED,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,EAAE,QAAQ,EACrB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,KAAK,GAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAa;IAQ7D,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IASnD,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;YAkB7H,cAAc;IAc5B,OAAO,CAAC,OAAO;CAahB;AAED,8HAA8H;AAC9H,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAsBtG;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,CAAC,mBAAmB,CAAC,GAAG,gBAAgB,EAAE,CAQ/F;AAED,+FAA+F;AAC/F,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAYrH"}
|