@superblocksteam/shared 0.9617.8 → 0.9618.1
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/database-lifecycle/appDatabaseImport.d.ts +42 -7
- package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.js +66 -5
- package/dist/database-lifecycle/appDatabaseImport.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.test.js +79 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseStatus.d.ts +18 -0
- package/dist/database-lifecycle/appDatabaseStatus.d.ts.map +1 -1
- package/dist/database-lifecycle/appDatabaseStatus.js +21 -0
- package/dist/database-lifecycle/appDatabaseStatus.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseStatus.test.js +11 -0
- package/dist/database-lifecycle/appDatabaseStatus.test.js.map +1 -1
- package/dist/database-lifecycle/dataImport.d.ts +45 -2
- package/dist/database-lifecycle/dataImport.d.ts.map +1 -1
- package/dist/database-lifecycle/dataImport.js +209 -3
- package/dist/database-lifecycle/dataImport.js.map +1 -1
- package/dist/database-lifecycle/dataImport.test.js +60 -0
- package/dist/database-lifecycle/dataImport.test.js.map +1 -1
- package/dist/database-lifecycle/importCompatibility.d.ts +40 -0
- package/dist/database-lifecycle/importCompatibility.d.ts.map +1 -0
- package/dist/database-lifecycle/importCompatibility.js +121 -0
- package/dist/database-lifecycle/importCompatibility.js.map +1 -0
- package/dist/database-lifecycle/importCompatibility.test.d.ts +2 -0
- package/dist/database-lifecycle/importCompatibility.test.d.ts.map +1 -0
- package/dist/database-lifecycle/importCompatibility.test.js +114 -0
- package/dist/database-lifecycle/importCompatibility.test.js.map +1 -0
- package/dist/database-lifecycle/index.d.ts +29 -5
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +18 -5
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +4 -1
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/audit/ocsf.d.ts +2 -1
- package/dist/types/audit/ocsf.d.ts.map +1 -1
- package/dist/types/audit/ocsf.js +8 -0
- package/dist/types/audit/ocsf.js.map +1 -1
- package/dist/types/codeSecurity/index.d.ts +2 -1
- package/dist/types/codeSecurity/index.d.ts.map +1 -1
- package/dist/types/codeSecurity/index.js +43 -5
- package/dist/types/codeSecurity/index.js.map +1 -1
- package/dist/types/codeSecurity/index.test.js +37 -3
- package/dist/types/codeSecurity/index.test.js.map +1 -1
- package/dist/types/datasource/auth.d.ts +1 -0
- package/dist/types/datasource/auth.d.ts.map +1 -1
- package/dist/types/datasource/auth.js +8 -2
- package/dist/types/datasource/auth.js.map +1 -1
- package/dist/types/datasource/auth.test.js +19 -1
- package/dist/types/datasource/auth.test.js.map +1 -1
- package/dist/types/integration/index.d.ts +16 -1
- package/dist/types/integration/index.d.ts.map +1 -1
- package/dist/types/integration/index.js +8 -1
- package/dist/types/integration/index.js.map +1 -1
- package/dist/types/policyGate/index.d.ts +1 -0
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +3 -1
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/types/publishPipeline/index.d.ts +2 -0
- package/dist/types/publishPipeline/index.d.ts.map +1 -1
- package/dist/types/publishPipeline/index.js.map +1 -1
- package/dist/types/publishPipeline/plan.d.ts +1 -1
- package/dist/types/publishPipeline/plan.d.ts.map +1 -1
- package/dist/types/publishPipeline/plan.js.map +1 -1
- package/dist/types/rbac/index.d.ts +0 -5
- package/dist/types/rbac/index.d.ts.map +1 -1
- package/dist/types/rbac/index.js.map +1 -1
- package/dist/types/response/index.d.ts +0 -1
- package/dist/types/response/index.d.ts.map +1 -1
- package/dist/types/response/index.js +0 -1
- package/dist/types/response/index.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +42 -7
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.js +65 -5
- package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js +79 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts +18 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseStatus.js +18 -0
- package/dist-esm/database-lifecycle/appDatabaseStatus.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js +12 -1
- package/dist-esm/database-lifecycle/appDatabaseStatus.test.js.map +1 -1
- package/dist-esm/database-lifecycle/dataImport.d.ts +45 -2
- package/dist-esm/database-lifecycle/dataImport.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/dataImport.js +205 -3
- package/dist-esm/database-lifecycle/dataImport.js.map +1 -1
- package/dist-esm/database-lifecycle/dataImport.test.js +61 -1
- package/dist-esm/database-lifecycle/dataImport.test.js.map +1 -1
- package/dist-esm/database-lifecycle/importCompatibility.d.ts +40 -0
- package/dist-esm/database-lifecycle/importCompatibility.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/importCompatibility.js +117 -0
- package/dist-esm/database-lifecycle/importCompatibility.js.map +1 -0
- package/dist-esm/database-lifecycle/importCompatibility.test.d.ts +2 -0
- package/dist-esm/database-lifecycle/importCompatibility.test.d.ts.map +1 -0
- package/dist-esm/database-lifecycle/importCompatibility.test.js +112 -0
- package/dist-esm/database-lifecycle/importCompatibility.test.js.map +1 -0
- package/dist-esm/database-lifecycle/index.d.ts +29 -5
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +13 -5
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +4 -1
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/index.d.ts +1 -0
- package/dist-esm/index.d.ts.map +1 -1
- package/dist-esm/index.js +1 -0
- package/dist-esm/index.js.map +1 -1
- package/dist-esm/types/audit/ocsf.d.ts +2 -1
- package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
- package/dist-esm/types/audit/ocsf.js +8 -0
- package/dist-esm/types/audit/ocsf.js.map +1 -1
- package/dist-esm/types/codeSecurity/index.d.ts +2 -1
- package/dist-esm/types/codeSecurity/index.d.ts.map +1 -1
- package/dist-esm/types/codeSecurity/index.js +43 -5
- package/dist-esm/types/codeSecurity/index.js.map +1 -1
- package/dist-esm/types/codeSecurity/index.test.js +37 -3
- package/dist-esm/types/codeSecurity/index.test.js.map +1 -1
- package/dist-esm/types/datasource/auth.d.ts +1 -0
- package/dist-esm/types/datasource/auth.d.ts.map +1 -1
- package/dist-esm/types/datasource/auth.js +7 -1
- package/dist-esm/types/datasource/auth.js.map +1 -1
- package/dist-esm/types/datasource/auth.test.js +20 -2
- package/dist-esm/types/datasource/auth.test.js.map +1 -1
- package/dist-esm/types/integration/index.d.ts +16 -1
- package/dist-esm/types/integration/index.d.ts.map +1 -1
- package/dist-esm/types/integration/index.js +8 -1
- package/dist-esm/types/integration/index.js.map +1 -1
- package/dist-esm/types/policyGate/index.d.ts +1 -0
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +2 -0
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/types/publishPipeline/index.d.ts +2 -0
- package/dist-esm/types/publishPipeline/index.d.ts.map +1 -1
- package/dist-esm/types/publishPipeline/index.js.map +1 -1
- package/dist-esm/types/publishPipeline/plan.d.ts +1 -1
- package/dist-esm/types/publishPipeline/plan.d.ts.map +1 -1
- package/dist-esm/types/publishPipeline/plan.js.map +1 -1
- package/dist-esm/types/rbac/index.d.ts +0 -5
- package/dist-esm/types/rbac/index.d.ts.map +1 -1
- package/dist-esm/types/rbac/index.js.map +1 -1
- package/dist-esm/types/response/index.d.ts +0 -1
- package/dist-esm/types/response/index.d.ts.map +1 -1
- package/dist-esm/types/response/index.js +0 -1
- package/dist-esm/types/response/index.js.map +1 -1
- package/package.json +2 -2
- package/src/database-lifecycle/appDatabaseImport.test.ts +95 -0
- package/src/database-lifecycle/appDatabaseImport.ts +134 -8
- package/src/database-lifecycle/appDatabaseStatus.test.ts +20 -1
- package/src/database-lifecycle/appDatabaseStatus.ts +39 -0
- package/src/database-lifecycle/dataImport.test.ts +75 -0
- package/src/database-lifecycle/dataImport.ts +267 -4
- package/src/database-lifecycle/importCompatibility.test.ts +146 -0
- package/src/database-lifecycle/importCompatibility.ts +148 -0
- package/src/database-lifecycle/index.test.ts +4 -1
- package/src/database-lifecycle/index.ts +54 -4
- package/src/index.ts +1 -0
- package/src/types/audit/ocsf.ts +10 -1
- package/src/types/codeSecurity/index.test.ts +40 -3
- package/src/types/codeSecurity/index.ts +45 -4
- package/src/types/datasource/auth.test.ts +25 -1
- package/src/types/datasource/auth.ts +9 -1
- package/src/types/integration/index.ts +17 -0
- package/src/types/policyGate/index.ts +3 -0
- package/src/types/publishPipeline/index.ts +2 -0
- package/src/types/publishPipeline/plan.ts +1 -1
- package/src/types/rbac/index.ts +0 -8
- package/src/types/response/index.ts +0 -1
- package/dist/types/response/GetGroupPermissionEntitiesResponseBody.d.ts +0 -3
- package/dist/types/response/GetGroupPermissionEntitiesResponseBody.d.ts.map +0 -1
- package/dist/types/response/GetGroupPermissionEntitiesResponseBody.js +0 -3
- package/dist/types/response/GetGroupPermissionEntitiesResponseBody.js.map +0 -1
- package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.d.ts +0 -3
- package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.d.ts.map +0 -1
- package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.js +0 -2
- package/dist-esm/types/response/GetGroupPermissionEntitiesResponseBody.js.map +0 -1
- package/src/types/response/GetGroupPermissionEntitiesResponseBody.ts +0 -3
|
@@ -13,6 +13,8 @@ export const DATA_IMPORT_STATES = [
|
|
|
13
13
|
'validating',
|
|
14
14
|
'validating_schema',
|
|
15
15
|
'schema_validated',
|
|
16
|
+
'capturing_source',
|
|
17
|
+
'restoring',
|
|
16
18
|
'ready',
|
|
17
19
|
'importing',
|
|
18
20
|
'completed',
|
|
@@ -60,19 +62,51 @@ export type DataImportUploadMetadata = {
|
|
|
60
62
|
// signed URLs, credentials, ETags, and row samples stay out.
|
|
61
63
|
export type PgDumpDataImportMetadata = {
|
|
62
64
|
kind: 'pg_dump_custom';
|
|
65
|
+
capturedObjectKey?: string;
|
|
66
|
+
capturedSha256?: string;
|
|
67
|
+
capturedSizeBytes?: number;
|
|
63
68
|
currentPrefixSizeBytes?: number;
|
|
64
69
|
postgresMajorVersion?: number;
|
|
65
70
|
prefixChecksumSha256?: string;
|
|
66
71
|
prefixObjectKey?: string;
|
|
67
72
|
requestedPrefixSizeBytes?: number;
|
|
73
|
+
source?: {
|
|
74
|
+
integrationConfigurationId: string;
|
|
75
|
+
integrationId: string;
|
|
76
|
+
profile: string;
|
|
77
|
+
profileId: string;
|
|
78
|
+
type: 'integration';
|
|
79
|
+
};
|
|
68
80
|
tableNames?: string[];
|
|
69
81
|
};
|
|
70
82
|
|
|
83
|
+
export type CsvColumnMapping = {
|
|
84
|
+
source: string;
|
|
85
|
+
target: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Settings the table-import worker requires before it COPY-loads one CSV.
|
|
89
|
+
// Absent on uploads that only store the file.
|
|
90
|
+
export type CsvTableImportPlan = {
|
|
91
|
+
columnMapping: CsvColumnMapping[];
|
|
92
|
+
dateStyle: string;
|
|
93
|
+
delimiter: ',' | ';' | '\t' | '|';
|
|
94
|
+
emptyAsNull: boolean;
|
|
95
|
+
encoding: 'UTF8' | 'WINDOWS1252';
|
|
96
|
+
hasHeader: boolean;
|
|
97
|
+
timeZone: string;
|
|
98
|
+
trimWhitespace: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
71
101
|
export type CsvDataImportMetadata = {
|
|
72
102
|
kind: 'csv';
|
|
73
103
|
mode: 'append' | 'replace';
|
|
74
104
|
table: string;
|
|
75
|
-
}
|
|
105
|
+
} & Partial<CsvTableImportPlan>;
|
|
106
|
+
|
|
107
|
+
export type CsvImportPlanParse = { message: string; status: 'invalid' } | { plan: CsvTableImportPlan; status: 'ok' };
|
|
108
|
+
|
|
109
|
+
export type CsvTableImportPlanPresence = { status: 'absent' } | CsvImportPlanParse;
|
|
76
110
|
|
|
77
111
|
export type DataImportMetadata = PgDumpDataImportMetadata | CsvDataImportMetadata;
|
|
78
112
|
|
|
@@ -122,14 +156,119 @@ export function parseDataImportMetadata(kind: DataImportKind, value: unknown): D
|
|
|
122
156
|
return parsePgDumpMetadata(value);
|
|
123
157
|
}
|
|
124
158
|
const table = canonicalCsvTable(value.table);
|
|
125
|
-
if (table === undefined || (value.mode !== 'append' && value.mode !== 'replace')) {
|
|
159
|
+
if (table === undefined || (value.mode !== 'append' && value.mode !== 'replace') || isReservedCsvImportTable(table)) {
|
|
126
160
|
return undefined;
|
|
127
161
|
}
|
|
128
|
-
const
|
|
162
|
+
const plan = parseCsvTableImportPlanFields(value);
|
|
163
|
+
if (plan.status === 'invalid') {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const allowed = new Set<string>(['kind', 'mode', 'table', ...(plan.status === 'ok' ? CSV_TABLE_IMPORT_PLAN_KEYS : [])]);
|
|
129
167
|
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
130
168
|
return undefined;
|
|
131
169
|
}
|
|
132
|
-
|
|
170
|
+
if (plan.status === 'absent') {
|
|
171
|
+
return { kind: 'csv', mode: value.mode, table };
|
|
172
|
+
}
|
|
173
|
+
return { kind: 'csv', mode: value.mode, table, ...plan.plan };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function csvTableImportPlan(metadata: CsvDataImportMetadata): CsvTableImportPlan | undefined {
|
|
177
|
+
if (
|
|
178
|
+
metadata.columnMapping === undefined ||
|
|
179
|
+
metadata.dateStyle === undefined ||
|
|
180
|
+
metadata.delimiter === undefined ||
|
|
181
|
+
metadata.emptyAsNull === undefined ||
|
|
182
|
+
metadata.encoding === undefined ||
|
|
183
|
+
metadata.hasHeader === undefined ||
|
|
184
|
+
metadata.timeZone === undefined ||
|
|
185
|
+
metadata.trimWhitespace === undefined
|
|
186
|
+
) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
columnMapping: metadata.columnMapping,
|
|
191
|
+
dateStyle: metadata.dateStyle,
|
|
192
|
+
delimiter: metadata.delimiter,
|
|
193
|
+
emptyAsNull: metadata.emptyAsNull,
|
|
194
|
+
encoding: metadata.encoding,
|
|
195
|
+
hasHeader: metadata.hasHeader,
|
|
196
|
+
timeZone: metadata.timeZone,
|
|
197
|
+
trimWhitespace: metadata.trimWhitespace
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The migrations ledger and system catalogs are not import targets.
|
|
202
|
+
export function isReservedCsvImportTable(table: string): boolean {
|
|
203
|
+
const separator = table.indexOf('.');
|
|
204
|
+
if (separator < 1) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const schema = table.slice(0, separator);
|
|
208
|
+
const name = table.slice(separator + 1);
|
|
209
|
+
if (schema === 'information_schema' || schema.startsWith('pg_')) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return schema === 'public' && name === 'superblocks_schema_migrations';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const CSV_DATE_FORMAT_TO_DATE_STYLE: Record<string, string> = {
|
|
216
|
+
'DD.MM.YYYY': 'German, DMY',
|
|
217
|
+
'DD/MM/YYYY': 'ISO, DMY',
|
|
218
|
+
'MM/DD/YYYY': 'ISO, MDY',
|
|
219
|
+
'YYYY-MM-DD': 'ISO, YMD'
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Wizard spellings become the worker's DateStyle, encoding, and column list.
|
|
223
|
+
// emptyAsNull stays on, the delimiter stays a comma, and the session time zone
|
|
224
|
+
// stays UTC. The first row is always a header.
|
|
225
|
+
export function csvImportPlanFromWizard(input: {
|
|
226
|
+
columnMappings: Record<string, string>;
|
|
227
|
+
dateFormat: string;
|
|
228
|
+
encoding: string;
|
|
229
|
+
trimWhitespace: boolean;
|
|
230
|
+
}): CsvImportPlanParse {
|
|
231
|
+
const encoding = workerCsvEncoding(input.encoding);
|
|
232
|
+
if (!encoding) {
|
|
233
|
+
return { message: 'Encoding must be UTF-8 or Windows-1252.', status: 'invalid' };
|
|
234
|
+
}
|
|
235
|
+
const dateStyle = workerDateStyle(input.dateFormat);
|
|
236
|
+
if (!dateStyle) {
|
|
237
|
+
return {
|
|
238
|
+
message: 'Date format must be YYYY-MM-DD, MM/DD/YYYY, DD/MM/YYYY, or DD.MM.YYYY.',
|
|
239
|
+
status: 'invalid'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const columnMapping = columnMappingsFromWizard(input.columnMappings);
|
|
243
|
+
if (columnMapping.status === 'invalid') {
|
|
244
|
+
return columnMapping;
|
|
245
|
+
}
|
|
246
|
+
return readCsvTableImportPlan({
|
|
247
|
+
columnMapping: columnMapping.mapping,
|
|
248
|
+
dateStyle,
|
|
249
|
+
delimiter: ',',
|
|
250
|
+
emptyAsNull: true,
|
|
251
|
+
encoding,
|
|
252
|
+
hasHeader: true,
|
|
253
|
+
timeZone: 'UTC',
|
|
254
|
+
trimWhitespace: input.trimWhitespace
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function parseCsvTableImportPlanFields(value: Record<string, unknown>): CsvTableImportPlanPresence {
|
|
259
|
+
if (!CSV_TABLE_IMPORT_PLAN_KEYS.some((key) => value[key] !== undefined)) {
|
|
260
|
+
return { status: 'absent' };
|
|
261
|
+
}
|
|
262
|
+
return readCsvTableImportPlan({
|
|
263
|
+
columnMapping: value.columnMapping,
|
|
264
|
+
dateStyle: value.dateStyle,
|
|
265
|
+
delimiter: value.delimiter,
|
|
266
|
+
emptyAsNull: value.emptyAsNull,
|
|
267
|
+
encoding: value.encoding,
|
|
268
|
+
hasHeader: value.hasHeader,
|
|
269
|
+
timeZone: value.timeZone,
|
|
270
|
+
trimWhitespace: value.trimWhitespace
|
|
271
|
+
});
|
|
133
272
|
}
|
|
134
273
|
|
|
135
274
|
export function parseDataImportOutcome(value: unknown): DataImportOutcome | undefined {
|
|
@@ -235,6 +374,130 @@ function assignOptionalInteger(
|
|
|
235
374
|
return true;
|
|
236
375
|
}
|
|
237
376
|
|
|
377
|
+
const CSV_TABLE_IMPORT_PLAN_KEYS = [
|
|
378
|
+
'columnMapping',
|
|
379
|
+
'dateStyle',
|
|
380
|
+
'delimiter',
|
|
381
|
+
'emptyAsNull',
|
|
382
|
+
'encoding',
|
|
383
|
+
'hasHeader',
|
|
384
|
+
'timeZone',
|
|
385
|
+
'trimWhitespace'
|
|
386
|
+
] as const;
|
|
387
|
+
|
|
388
|
+
const CSV_DATE_STYLE = /^(ISO|SQL|Postgres|German)(, (MDY|DMY|YMD))?$/;
|
|
389
|
+
const CSV_TIME_ZONE = /^[A-Za-z0-9_+-]{1,64}(?:\/[A-Za-z0-9_+-]{1,64}){0,2}$/;
|
|
390
|
+
const CSV_TARGET_COLUMN = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
391
|
+
const CSV_DELIMITERS = [',', ';', '\t', '|'] as const;
|
|
392
|
+
|
|
393
|
+
function workerCsvEncoding(value: string): 'UTF8' | 'WINDOWS1252' | undefined {
|
|
394
|
+
const normalized = value.trim().toUpperCase().replace(/-/g, '');
|
|
395
|
+
if (normalized === 'UTF8') {
|
|
396
|
+
return 'UTF8';
|
|
397
|
+
}
|
|
398
|
+
if (normalized === 'WINDOWS1252' || normalized === 'CP1252') {
|
|
399
|
+
return 'WINDOWS1252';
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function workerDateStyle(value: string): string | undefined {
|
|
405
|
+
const trimmed = value.trim();
|
|
406
|
+
return CSV_DATE_FORMAT_TO_DATE_STYLE[trimmed] ?? (CSV_DATE_STYLE.test(trimmed) ? trimmed : undefined);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function columnMappingsFromWizard(
|
|
410
|
+
mappings: Record<string, string>
|
|
411
|
+
): { mapping: CsvColumnMapping[]; status: 'ok' } | { message: string; status: 'invalid' } {
|
|
412
|
+
const columnMapping: CsvColumnMapping[] = [];
|
|
413
|
+
const targets = new Set<string>();
|
|
414
|
+
for (const [source, target] of Object.entries(mappings)) {
|
|
415
|
+
const trimmedTarget = target.trim();
|
|
416
|
+
if (trimmedTarget.length === 0) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const trimmedSource = source.trim();
|
|
420
|
+
if (trimmedSource.length === 0 || trimmedSource.length > 256) {
|
|
421
|
+
return { message: 'CSV column names must be 1 to 256 characters.', status: 'invalid' };
|
|
422
|
+
}
|
|
423
|
+
if (!CSV_TARGET_COLUMN.test(trimmedTarget)) {
|
|
424
|
+
return { message: 'Table column names must be identifiers.', status: 'invalid' };
|
|
425
|
+
}
|
|
426
|
+
const folded = trimmedTarget.toLowerCase();
|
|
427
|
+
if (targets.has(folded)) {
|
|
428
|
+
return { message: 'Each table column can only be mapped once.', status: 'invalid' };
|
|
429
|
+
}
|
|
430
|
+
targets.add(folded);
|
|
431
|
+
columnMapping.push({ source: trimmedSource, target: trimmedTarget });
|
|
432
|
+
}
|
|
433
|
+
columnMapping.sort((left, right) => (left.source < right.source ? -1 : left.source > right.source ? 1 : 0));
|
|
434
|
+
if (columnMapping.length === 0) {
|
|
435
|
+
return { message: 'Map at least one CSV column to a table column.', status: 'invalid' };
|
|
436
|
+
}
|
|
437
|
+
return { mapping: columnMapping, status: 'ok' };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function readCsvTableImportPlan(value: {
|
|
441
|
+
columnMapping: unknown;
|
|
442
|
+
dateStyle: unknown;
|
|
443
|
+
delimiter: unknown;
|
|
444
|
+
emptyAsNull: unknown;
|
|
445
|
+
encoding: unknown;
|
|
446
|
+
hasHeader: unknown;
|
|
447
|
+
timeZone: unknown;
|
|
448
|
+
trimWhitespace: unknown;
|
|
449
|
+
}): CsvImportPlanParse {
|
|
450
|
+
if (!Array.isArray(value.columnMapping) || value.columnMapping.length === 0) {
|
|
451
|
+
return { message: 'Map at least one CSV column to a table column.', status: 'invalid' };
|
|
452
|
+
}
|
|
453
|
+
const columnMapping: CsvColumnMapping[] = [];
|
|
454
|
+
const targets = new Set<string>();
|
|
455
|
+
for (const entry of value.columnMapping) {
|
|
456
|
+
if (!isUnknownRecord(entry) || typeof entry.source !== 'string' || typeof entry.target !== 'string') {
|
|
457
|
+
return { message: 'CSV import settings are incomplete or not supported.', status: 'invalid' };
|
|
458
|
+
}
|
|
459
|
+
const source = entry.source.trim();
|
|
460
|
+
const target = entry.target.trim();
|
|
461
|
+
if (source.length === 0 || source.length > 256 || !CSV_TARGET_COLUMN.test(target)) {
|
|
462
|
+
return { message: 'CSV import settings are incomplete or not supported.', status: 'invalid' };
|
|
463
|
+
}
|
|
464
|
+
const folded = target.toLowerCase();
|
|
465
|
+
if (targets.has(folded)) {
|
|
466
|
+
return { message: 'Each table column can only be mapped once.', status: 'invalid' };
|
|
467
|
+
}
|
|
468
|
+
targets.add(folded);
|
|
469
|
+
columnMapping.push({ source, target });
|
|
470
|
+
}
|
|
471
|
+
if (typeof value.dateStyle !== 'string' || !CSV_DATE_STYLE.test(value.dateStyle)) {
|
|
472
|
+
return { message: 'Date format must be YYYY-MM-DD, MM/DD/YYYY, DD/MM/YYYY, or DD.MM.YYYY.', status: 'invalid' };
|
|
473
|
+
}
|
|
474
|
+
if (typeof value.delimiter !== 'string' || !CSV_DELIMITERS.includes(value.delimiter as (typeof CSV_DELIMITERS)[number])) {
|
|
475
|
+
return { message: 'CSV import settings are incomplete or not supported.', status: 'invalid' };
|
|
476
|
+
}
|
|
477
|
+
if (value.encoding !== 'UTF8' && value.encoding !== 'WINDOWS1252') {
|
|
478
|
+
return { message: 'Encoding must be UTF-8 or Windows-1252.', status: 'invalid' };
|
|
479
|
+
}
|
|
480
|
+
if (typeof value.timeZone !== 'string' || !CSV_TIME_ZONE.test(value.timeZone)) {
|
|
481
|
+
return { message: 'CSV import settings are incomplete or not supported.', status: 'invalid' };
|
|
482
|
+
}
|
|
483
|
+
if (typeof value.emptyAsNull !== 'boolean' || typeof value.hasHeader !== 'boolean' || typeof value.trimWhitespace !== 'boolean') {
|
|
484
|
+
return { message: 'CSV import settings are incomplete or not supported.', status: 'invalid' };
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
plan: {
|
|
488
|
+
columnMapping,
|
|
489
|
+
dateStyle: value.dateStyle,
|
|
490
|
+
delimiter: value.delimiter as CsvTableImportPlan['delimiter'],
|
|
491
|
+
emptyAsNull: value.emptyAsNull,
|
|
492
|
+
encoding: value.encoding,
|
|
493
|
+
hasHeader: value.hasHeader,
|
|
494
|
+
timeZone: value.timeZone,
|
|
495
|
+
trimWhitespace: value.trimWhitespace
|
|
496
|
+
},
|
|
497
|
+
status: 'ok'
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
238
501
|
function isDecimalCount(value: unknown): value is string {
|
|
239
502
|
return typeof value === 'string' && /^(0|[1-9][0-9]*)$/.test(value);
|
|
240
503
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AgentStatus } from '../types/organization/agent.js';
|
|
4
|
+
import { DATABASE_LIFECYCLE_TAG_IMPORT_TRANSFORMS, evaluateAppDatabaseImportTag } from './importCompatibility.js';
|
|
5
|
+
|
|
6
|
+
const IMPORT_OPERATIONS = ['complete_import_upload', 'describe_schema', 'import_table_data', 'prepare_import_upload'];
|
|
7
|
+
|
|
8
|
+
function agent({
|
|
9
|
+
agentName,
|
|
10
|
+
operations = IMPORT_OPERATIONS,
|
|
11
|
+
profiles = ['production'],
|
|
12
|
+
status = AgentStatus.ACTIVE,
|
|
13
|
+
transforms = ['empty_as_null', 'trim_whitespace']
|
|
14
|
+
}: {
|
|
15
|
+
agentName?: string;
|
|
16
|
+
operations?: string[];
|
|
17
|
+
profiles?: string[];
|
|
18
|
+
status?: AgentStatus | string;
|
|
19
|
+
transforms?: string[];
|
|
20
|
+
} = {}) {
|
|
21
|
+
return {
|
|
22
|
+
status,
|
|
23
|
+
tags: {
|
|
24
|
+
...(agentName === undefined ? {} : { 'databaseLifecycle:agentName': [agentName] }),
|
|
25
|
+
'databaseLifecycle:operations': operations,
|
|
26
|
+
[DATABASE_LIFECYCLE_TAG_IMPORT_TRANSFORMS]: transforms,
|
|
27
|
+
profile: profiles
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('evaluateAppDatabaseImportTag', () => {
|
|
33
|
+
it('offers import and every advertised transform when one agent can run it', () => {
|
|
34
|
+
expect(evaluateAppDatabaseImportTag({ agents: [agent()], profileKey: 'production' })).toEqual({
|
|
35
|
+
availableTransforms: ['empty_as_null', 'trim_whitespace'],
|
|
36
|
+
missingOperations: [],
|
|
37
|
+
profileKey: 'production',
|
|
38
|
+
reason: '',
|
|
39
|
+
supportState: 'supported'
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('ignores a worker that does not own the data tag', () => {
|
|
44
|
+
const support = evaluateAppDatabaseImportTag({
|
|
45
|
+
agentName: 'owner',
|
|
46
|
+
agents: [agent({ agentName: 'other' }), agent({ agentName: 'owner', operations: ['describe_schema'] })],
|
|
47
|
+
profileKey: 'production'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(support.supportState).toBe('unsupported');
|
|
51
|
+
expect(support.missingOperations).toEqual(['complete_import_upload', 'import_table_data', 'prepare_import_upload']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps a transform the owner advertises when another worker omits it', () => {
|
|
55
|
+
const support = evaluateAppDatabaseImportTag({
|
|
56
|
+
agentName: 'owner',
|
|
57
|
+
agents: [agent({ agentName: 'other', transforms: ['empty_as_null'] }), agent({ agentName: 'owner' })],
|
|
58
|
+
profileKey: 'production'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(support.supportState).toBe('supported');
|
|
62
|
+
expect(support.availableTransforms).toEqual(['empty_as_null', 'trim_whitespace']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('waits when the owning worker is not connected', () => {
|
|
66
|
+
const support = evaluateAppDatabaseImportTag({
|
|
67
|
+
agentName: 'owner',
|
|
68
|
+
agents: [agent({ agentName: 'other' })],
|
|
69
|
+
profileKey: 'production'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(support.supportState).toBe('temporarily_unavailable');
|
|
73
|
+
expect(support.reason).toContain('Its worker is not connected');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('disables CSV when the worker does not advertise empty_as_null', () => {
|
|
77
|
+
const support = evaluateAppDatabaseImportTag({
|
|
78
|
+
agents: [agent({ transforms: ['trim_whitespace'] })],
|
|
79
|
+
profileKey: 'production'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(support.supportState).toBe('unsupported');
|
|
83
|
+
expect(support.availableTransforms).toEqual([]);
|
|
84
|
+
expect(support.reason).toContain('empty_as_null');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('omits trim when a replica that can claim the import does not advertise it', () => {
|
|
88
|
+
const support = evaluateAppDatabaseImportTag({
|
|
89
|
+
agentName: 'owner',
|
|
90
|
+
agents: [
|
|
91
|
+
agent({ agentName: 'owner' }),
|
|
92
|
+
agent({
|
|
93
|
+
agentName: 'owner',
|
|
94
|
+
operations: ['import_table_data'],
|
|
95
|
+
transforms: ['empty_as_null']
|
|
96
|
+
})
|
|
97
|
+
],
|
|
98
|
+
profileKey: 'production'
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(support.supportState).toBe('supported');
|
|
102
|
+
expect(support.availableTransforms).toEqual(['empty_as_null']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('disables CSV when a replica that can claim the import omits empty_as_null', () => {
|
|
106
|
+
const support = evaluateAppDatabaseImportTag({
|
|
107
|
+
agentName: 'owner',
|
|
108
|
+
agents: [agent({ agentName: 'owner' }), agent({ agentName: 'owner', operations: ['import_table_data'], transforms: [] })],
|
|
109
|
+
profileKey: 'production'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(support.supportState).toBe('unsupported');
|
|
113
|
+
expect(support.availableTransforms).toEqual([]);
|
|
114
|
+
expect(support.reason).toContain('empty_as_null');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('leaves out a transform that any capable agent does not advertise', () => {
|
|
118
|
+
const support = evaluateAppDatabaseImportTag({
|
|
119
|
+
agents: [agent(), agent({ transforms: ['empty_as_null'] })],
|
|
120
|
+
profileKey: 'production'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(support.supportState).toBe('supported');
|
|
124
|
+
expect(support.availableTransforms).toEqual(['empty_as_null']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('disables a tag whose connected agent is missing an import operation', () => {
|
|
128
|
+
const support = evaluateAppDatabaseImportTag({
|
|
129
|
+
agents: [agent({ operations: ['describe_schema', 'ensure_database', 'migrate_schema', 'prepare_import_upload'] })],
|
|
130
|
+
profileKey: 'production'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(support.supportState).toBe('unsupported');
|
|
134
|
+
expect(support.missingOperations).toEqual(['complete_import_upload', 'import_table_data']);
|
|
135
|
+
expect(support.reason).toContain('does not advertise the import operations');
|
|
136
|
+
expect(support.availableTransforms).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('reports that the check could not tell when no agent is connected', () => {
|
|
140
|
+
expect(evaluateAppDatabaseImportTag({ agents: undefined, profileKey: 'production' }).supportState).toBe('temporarily_unavailable');
|
|
141
|
+
expect(evaluateAppDatabaseImportTag({ agents: [], profileKey: 'production' }).supportState).toBe('temporarily_unavailable');
|
|
142
|
+
expect(
|
|
143
|
+
evaluateAppDatabaseImportTag({ agents: [agent({ status: AgentStatus.DISCONNECTED })], profileKey: 'production' }).supportState
|
|
144
|
+
).toBe('temporarily_unavailable');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AgentStatus } from '../types/organization/agent.js';
|
|
2
|
+
import type { AppDatabaseTagSupportState } from './appDatabaseCompatibility.js';
|
|
3
|
+
import type { AgentLifecycleTagSource } from './edit-lifecycle-capability.js';
|
|
4
|
+
import { DATABASE_LIFECYCLE_TAG_AGENT_NAME } from './physicalPoolIdentity.js';
|
|
5
|
+
|
|
6
|
+
export const DATABASE_LIFECYCLE_TAG_IMPORT_TRANSFORMS = 'databaseLifecycle:importTransforms';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Operations a data tag needs before CSV import can be offered.
|
|
10
|
+
*
|
|
11
|
+
* Kept off {@link APP_DATABASE_REQUIRED_OPERATIONS}. Missing one of these
|
|
12
|
+
* disables import. It must not make the app undeployable.
|
|
13
|
+
*/
|
|
14
|
+
export const APP_DATABASE_IMPORT_REQUIRED_OPERATIONS = ['complete_import_upload', 'import_table_data', 'prepare_import_upload'] as const;
|
|
15
|
+
|
|
16
|
+
export const APP_DATABASE_IMPORT_TRANSFORMS = ['empty_as_null', 'trim_whitespace'] as const;
|
|
17
|
+
export type AppDatabaseImportTransform = (typeof APP_DATABASE_IMPORT_TRANSFORMS)[number];
|
|
18
|
+
|
|
19
|
+
export type AppDatabaseImportTagSupport = {
|
|
20
|
+
availableTransforms: AppDatabaseImportTransform[];
|
|
21
|
+
missingOperations: string[];
|
|
22
|
+
profileKey: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
supportState: AppDatabaseTagSupportState;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const IMPORT_UNAVAILABLE_REASON =
|
|
28
|
+
'CSV import is not available for this data tag. The connected agent does not advertise the import operations.';
|
|
29
|
+
const IMPORT_EMPTY_AS_NULL_REASON = 'CSV import is not available for this data tag. The connected agent does not advertise empty_as_null.';
|
|
30
|
+
const IMPORT_OWNER_DISCONNECTED_REASON = 'CSV import is not available for this data tag. Its worker is not connected.';
|
|
31
|
+
const IMPORT_UNKNOWN_REASON = 'CSV import could not be checked. No connected agent has published its capabilities.';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether one data tag can accept a CSV import, from the agents connected now.
|
|
35
|
+
*
|
|
36
|
+
* `unsupported` is a standing gap. `temporarily_unavailable` means nothing
|
|
37
|
+
* connected could be read, so the tag may start working without an app change.
|
|
38
|
+
* When `agentName` is set, only the worker that owns the binding counts.
|
|
39
|
+
* Replicas that publish that same name stay together. Schema description is an
|
|
40
|
+
* HTTP route, so `describe_schema` is not required here.
|
|
41
|
+
*
|
|
42
|
+
* The wizard always sends `empty_as_null`. CSV stays off unless every replica
|
|
43
|
+
* that can claim `import_table_data` advertises it. Optional transforms are
|
|
44
|
+
* the intersection of those same replicas, including one that can claim the
|
|
45
|
+
* copy while missing an upload operation. The server only hands a replica a
|
|
46
|
+
* dispatch for an operation it advertises.
|
|
47
|
+
*/
|
|
48
|
+
export function evaluateAppDatabaseImportTag({
|
|
49
|
+
agentName,
|
|
50
|
+
agents,
|
|
51
|
+
profileKey
|
|
52
|
+
}: {
|
|
53
|
+
agentName?: string | null;
|
|
54
|
+
agents: readonly AgentLifecycleTagSource[] | undefined;
|
|
55
|
+
profileKey: string;
|
|
56
|
+
}): AppDatabaseImportTagSupport {
|
|
57
|
+
const active = (agents ?? []).filter(isActiveAgent);
|
|
58
|
+
if (agents === undefined || active.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
availableTransforms: [],
|
|
61
|
+
missingOperations: [...APP_DATABASE_IMPORT_REQUIRED_OPERATIONS],
|
|
62
|
+
profileKey,
|
|
63
|
+
reason: IMPORT_UNKNOWN_REASON,
|
|
64
|
+
supportState: 'temporarily_unavailable'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const owner = agentName?.trim() ?? '';
|
|
68
|
+
const counted = owner === '' ? active : active.filter((agent) => publishesAgentName(agent, owner));
|
|
69
|
+
if (owner !== '' && counted.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
availableTransforms: [],
|
|
72
|
+
missingOperations: [...APP_DATABASE_IMPORT_REQUIRED_OPERATIONS],
|
|
73
|
+
profileKey,
|
|
74
|
+
reason: IMPORT_OWNER_DISCONNECTED_REASON,
|
|
75
|
+
supportState: 'temporarily_unavailable'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const covering = counted.filter((agent) => coversProfile(agent, profileKey));
|
|
79
|
+
const claimants = covering.filter((agent) => publishedOperations(agent).includes('import_table_data'));
|
|
80
|
+
const capable = claimants.filter((agent) => missingOperations(agent).length === 0);
|
|
81
|
+
if (capable.length === 0) {
|
|
82
|
+
const missing = covering.length === 0 ? [...APP_DATABASE_IMPORT_REQUIRED_OPERATIONS] : missingAcross(covering);
|
|
83
|
+
return {
|
|
84
|
+
availableTransforms: [],
|
|
85
|
+
missingOperations: missing,
|
|
86
|
+
profileKey,
|
|
87
|
+
reason: IMPORT_UNAVAILABLE_REASON,
|
|
88
|
+
supportState: 'unsupported'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const availableTransforms = transformsInCommon(claimants);
|
|
92
|
+
if (!availableTransforms.includes('empty_as_null')) {
|
|
93
|
+
return {
|
|
94
|
+
availableTransforms: [],
|
|
95
|
+
missingOperations: [],
|
|
96
|
+
profileKey,
|
|
97
|
+
reason: IMPORT_EMPTY_AS_NULL_REASON,
|
|
98
|
+
supportState: 'unsupported'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
availableTransforms,
|
|
103
|
+
missingOperations: [],
|
|
104
|
+
profileKey,
|
|
105
|
+
reason: '',
|
|
106
|
+
supportState: 'supported'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isActiveAgent(agent: AgentLifecycleTagSource): boolean {
|
|
111
|
+
return agent.status === AgentStatus.ACTIVE || agent.status === 'Active';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function publishesAgentName(agent: AgentLifecycleTagSource, agentName: string): boolean {
|
|
115
|
+
return (agent.tags?.[DATABASE_LIFECYCLE_TAG_AGENT_NAME] ?? []).includes(agentName);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function coversProfile(agent: AgentLifecycleTagSource, profileKey: string): boolean {
|
|
119
|
+
const profiles = agent.tags?.profile ?? [];
|
|
120
|
+
return profiles.includes('*') || profiles.includes(profileKey);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function publishedOperations(agent: AgentLifecycleTagSource): readonly string[] {
|
|
124
|
+
return agent.tags?.['databaseLifecycle:operations'] ?? [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function missingOperations(agent: AgentLifecycleTagSource): string[] {
|
|
128
|
+
const operations = publishedOperations(agent);
|
|
129
|
+
return APP_DATABASE_IMPORT_REQUIRED_OPERATIONS.filter((operation) => !operations.includes(operation));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function missingAcross(agents: readonly AgentLifecycleTagSource[]): string[] {
|
|
133
|
+
const lists = agents.map(missingOperations).sort((left, right) => left.length - right.length || left.join().localeCompare(right.join()));
|
|
134
|
+
return lists[0] ?? [...APP_DATABASE_IMPORT_REQUIRED_OPERATIONS];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function transformsInCommon(agents: readonly AgentLifecycleTagSource[]): AppDatabaseImportTransform[] {
|
|
138
|
+
const counts = new Map<AppDatabaseImportTransform, number>();
|
|
139
|
+
for (const agent of agents) {
|
|
140
|
+
const advertised = new Set(agent.tags?.[DATABASE_LIFECYCLE_TAG_IMPORT_TRANSFORMS] ?? []);
|
|
141
|
+
for (const transform of APP_DATABASE_IMPORT_TRANSFORMS) {
|
|
142
|
+
if (advertised.has(transform)) {
|
|
143
|
+
counts.set(transform, (counts.get(transform) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return APP_DATABASE_IMPORT_TRANSFORMS.filter((transform) => counts.get(transform) === agents.length);
|
|
148
|
+
}
|
|
@@ -33,8 +33,11 @@ describe('database lifecycle contracts', () => {
|
|
|
33
33
|
'complete_import_upload',
|
|
34
34
|
'prepare_import_prefix_upload',
|
|
35
35
|
'inspect_import_prefix',
|
|
36
|
+
'capture_integration_pg_dump',
|
|
37
|
+
'restore_integration_pg_dump',
|
|
36
38
|
'restore_import_data',
|
|
37
|
-
'describe_schema'
|
|
39
|
+
'describe_schema',
|
|
40
|
+
'import_table_data'
|
|
38
41
|
]);
|
|
39
42
|
});
|
|
40
43
|
|