georeference 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.fr.md +44 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE +21 -0
- package/README.fr.md +84 -0
- package/README.md +83 -2
- package/SECURITY.fr.md +9 -0
- package/SECURITY.md +9 -0
- package/dist/adapters/common.d.ts +5 -0
- package/dist/adapters/common.js +37 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/csv.d.ts +2 -0
- package/dist/adapters/csv.js +32 -0
- package/dist/adapters/csv.js.map +1 -0
- package/dist/adapters/geojson.d.ts +2 -0
- package/dist/adapters/geojson.js +36 -0
- package/dist/adapters/geojson.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/json.d.ts +3 -0
- package/dist/adapters/json.js +48 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/postgres.d.ts +3 -0
- package/dist/adapters/postgres.js +59 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +2 -0
- package/dist/adapters/sqlite.js +49 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/export.d.ts +6 -0
- package/dist/export.js +127 -0
- package/dist/export.js.map +1 -0
- package/dist/files.d.ts +6 -0
- package/dist/files.js +28 -0
- package/dist/files.js.map +1 -0
- package/dist/generate.d.ts +10 -0
- package/dist/generate.js +102 -0
- package/dist/generate.js.map +1 -0
- package/dist/graph.d.ts +3 -0
- package/dist/graph.js +31 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/package.d.ts +6 -0
- package/dist/package.js +133 -0
- package/dist/package.js.map +1 -0
- package/dist/seeding/index.d.ts +6 -0
- package/dist/seeding/index.js +80 -0
- package/dist/seeding/index.js.map +1 -0
- package/dist/seeding/ledger.d.ts +12 -0
- package/dist/seeding/ledger.js +54 -0
- package/dist/seeding/ledger.js.map +1 -0
- package/dist/seeding/mapping.d.ts +31 -0
- package/dist/seeding/mapping.js +54 -0
- package/dist/seeding/mapping.js.map +1 -0
- package/dist/seeding/preflight.d.ts +10 -0
- package/dist/seeding/preflight.js +149 -0
- package/dist/seeding/preflight.js.map +1 -0
- package/dist/seeding/retained.d.ts +5 -0
- package/dist/seeding/retained.js +32 -0
- package/dist/seeding/retained.js.map +1 -0
- package/dist/seeding/types.d.ts +41 -0
- package/dist/seeding/types.js +2 -0
- package/dist/seeding/types.js.map +1 -0
- package/dist/seeding/write.d.ts +4 -0
- package/dist/seeding/write.js +31 -0
- package/dist/seeding/write.js.map +1 -0
- package/dist/snapshot.d.ts +14 -0
- package/dist/snapshot.js +154 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/sources/geonames.d.ts +14 -0
- package/dist/sources/geonames.js +118 -0
- package/dist/sources/geonames.js.map +1 -0
- package/dist/types.d.ts +113 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +91 -0
- package/dist/validation.js.map +1 -0
- package/docs/architecture.fr.md +105 -0
- package/docs/architecture.md +105 -0
- package/docs/data-sources.fr.md +39 -0
- package/docs/data-sources.md +39 -0
- package/docs/format-decisions.fr.md +54 -0
- package/docs/format-decisions.md +54 -0
- package/docs/formats.fr.md +34 -0
- package/docs/formats.md +34 -0
- package/docs/generation.fr.md +29 -0
- package/docs/generation.md +29 -0
- package/docs/getting-started.fr.md +79 -0
- package/docs/getting-started.md +79 -0
- package/docs/seeding.fr.md +71 -0
- package/docs/seeding.md +71 -0
- package/examples/README.md +7 -0
- package/examples/metadata.json +13 -0
- package/examples/sample.ndjson +4 -0
- package/package.json +64 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { identifier, schemaSql } from "./mapping.js";
|
|
2
|
+
export async function assertCanStart(client, mapping, importKey) {
|
|
3
|
+
const exists = (await client.query('SELECT to_regclass($1) AS relation', [mapping.ledgerSql])).rows[0]?.relation != null;
|
|
4
|
+
if (!exists)
|
|
5
|
+
return;
|
|
6
|
+
const active = (await client.query(`SELECT import_key FROM ${mapping.ledgerSql} WHERE target_key=$1 AND status='in-progress' AND import_key<>$2 LIMIT 1`, [mapping.targetKey, importKey])).rows[0];
|
|
7
|
+
if (active)
|
|
8
|
+
throw new Error('An unfinished import must resume with the same package, mapping, mode and batchSize before another import can start');
|
|
9
|
+
}
|
|
10
|
+
export async function revisionFor(client, mapping, input, options) {
|
|
11
|
+
const exists = (await client.query('SELECT to_regclass($1) AS relation', [mapping.ledgerSql])).rows[0]?.relation != null;
|
|
12
|
+
if (!exists)
|
|
13
|
+
return options.revision ?? 0;
|
|
14
|
+
const latest = (await client.query(`SELECT package_id, revision FROM ${mapping.ledgerSql} WHERE target_key=$1 ORDER BY revision DESC LIMIT 1`, [mapping.targetKey])).rows[0];
|
|
15
|
+
const same = latest?.package_id === input.manifest.contentId;
|
|
16
|
+
const revision = options.revision ?? (same ? Number(latest?.revision) : 0);
|
|
17
|
+
if (latest && !options.allowDowngrade && (!same && revision <= Number(latest.revision) || same && revision < Number(latest.revision))) {
|
|
18
|
+
throw new Error(`Import revision must exceed ${String(latest.revision)} for changed content; datasetVersion labels are not ordered`);
|
|
19
|
+
}
|
|
20
|
+
return revision;
|
|
21
|
+
}
|
|
22
|
+
export async function setup(client, mapping, targetExists) {
|
|
23
|
+
await client.query('BEGIN');
|
|
24
|
+
try {
|
|
25
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${identifier(mapping.schema)}`);
|
|
26
|
+
if (!targetExists) {
|
|
27
|
+
await client.query(schemaSql(mapping));
|
|
28
|
+
await client.query(`CREATE INDEX ON ${mapping.tableSql} (${identifier(mapping.columns.parentId)})`);
|
|
29
|
+
}
|
|
30
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${mapping.ledgerSql} (
|
|
31
|
+
target_key TEXT NOT NULL, import_key TEXT NOT NULL, mapping_id TEXT NOT NULL,
|
|
32
|
+
package_id TEXT NOT NULL, dataset_version TEXT NOT NULL, revision BIGINT NOT NULL,
|
|
33
|
+
next_offset INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'in-progress',
|
|
34
|
+
PRIMARY KEY(target_key, import_key)
|
|
35
|
+
)`);
|
|
36
|
+
await client.query('COMMIT');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
await client.query('ROLLBACK');
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function beginImport(client, mapping, input, importKey, revision) {
|
|
44
|
+
const previous = (await client.query(`SELECT next_offset,status FROM ${mapping.ledgerSql} WHERE target_key=$1 AND import_key=$2`, [mapping.targetKey, importKey])).rows[0];
|
|
45
|
+
const offset = previous?.status === 'in-progress' ? Number(previous.next_offset) : 0;
|
|
46
|
+
await client.query(`INSERT INTO ${mapping.ledgerSql} (target_key,import_key,mapping_id,package_id,dataset_version,revision,next_offset,status)
|
|
47
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,'in-progress')
|
|
48
|
+
ON CONFLICT (target_key,import_key) DO UPDATE SET next_offset=EXCLUDED.next_offset,status='in-progress',revision=EXCLUDED.revision`, [mapping.targetKey, importKey, mapping.mappingId, input.manifest.contentId, input.manifest.datasetVersion, revision, offset]);
|
|
49
|
+
return { importKey, revision, offset };
|
|
50
|
+
}
|
|
51
|
+
export async function checkpoint(client, mapping, state, offset, complete) {
|
|
52
|
+
await client.query(`UPDATE ${mapping.ledgerSql} SET next_offset=$1,status=$2 WHERE target_key=$3 AND import_key=$4`, [offset, complete ? 'complete' : 'in-progress', mapping.targetKey, state.importKey]);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/seeding/ledger.ts"],"names":[],"mappings":"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","sourcesContent":["import type { QueryClient, SeedInput, SeedOptions } from './types.ts';\nimport { identifier, schemaSql } from './mapping.ts';\nimport type { ResolvedMapping } from './mapping.ts';\n\nexport interface ImportState { importKey: string; revision: number; offset: number }\n\nexport async function assertCanStart(client: QueryClient, mapping: ResolvedMapping, importKey: string): Promise<void> {\n const exists = (await client.query('SELECT to_regclass($1) AS relation', [mapping.ledgerSql])).rows[0]?.relation != null;\n if (!exists) return;\n const active = (await client.query(`SELECT import_key FROM ${mapping.ledgerSql} WHERE target_key=$1 AND status='in-progress' AND import_key<>$2 LIMIT 1`, [mapping.targetKey, importKey])).rows[0];\n if (active) throw new Error('An unfinished import must resume with the same package, mapping, mode and batchSize before another import can start');\n}\n\nexport async function revisionFor(client: QueryClient, mapping: ResolvedMapping, input: SeedInput, options: SeedOptions): Promise<number> {\n const exists = (await client.query('SELECT to_regclass($1) AS relation', [mapping.ledgerSql])).rows[0]?.relation != null;\n if (!exists) return options.revision ?? 0;\n const latest = (await client.query(`SELECT package_id, revision FROM ${mapping.ledgerSql} WHERE target_key=$1 ORDER BY revision DESC LIMIT 1`, [mapping.targetKey])).rows[0];\n const same = latest?.package_id === input.manifest.contentId;\n const revision = options.revision ?? (same ? Number(latest?.revision) : 0);\n if (latest && !options.allowDowngrade && (!same && revision <= Number(latest.revision) || same && revision < Number(latest.revision))) {\n throw new Error(`Import revision must exceed ${String(latest.revision)} for changed content; datasetVersion labels are not ordered`);\n }\n return revision;\n}\n\nexport async function setup(client: QueryClient, mapping: ResolvedMapping, targetExists: boolean): Promise<void> {\n await client.query('BEGIN');\n try {\n await client.query(`CREATE SCHEMA IF NOT EXISTS ${identifier(mapping.schema)}`);\n if (!targetExists) {\n await client.query(schemaSql(mapping));\n await client.query(`CREATE INDEX ON ${mapping.tableSql} (${identifier(mapping.columns.parentId)})`);\n }\n await client.query(`CREATE TABLE IF NOT EXISTS ${mapping.ledgerSql} (\n target_key TEXT NOT NULL, import_key TEXT NOT NULL, mapping_id TEXT NOT NULL,\n package_id TEXT NOT NULL, dataset_version TEXT NOT NULL, revision BIGINT NOT NULL,\n next_offset INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'in-progress',\n PRIMARY KEY(target_key, import_key)\n )`);\n await client.query('COMMIT');\n } catch (error) { await client.query('ROLLBACK'); throw error; }\n}\n\nexport async function beginImport(client: QueryClient, mapping: ResolvedMapping, input: SeedInput, importKey: string, revision: number): Promise<ImportState> {\n const previous = (await client.query(`SELECT next_offset,status FROM ${mapping.ledgerSql} WHERE target_key=$1 AND import_key=$2`, [mapping.targetKey, importKey])).rows[0];\n const offset = previous?.status === 'in-progress' ? Number(previous.next_offset) : 0;\n await client.query(`INSERT INTO ${mapping.ledgerSql} (target_key,import_key,mapping_id,package_id,dataset_version,revision,next_offset,status)\n VALUES ($1,$2,$3,$4,$5,$6,$7,'in-progress')\n ON CONFLICT (target_key,import_key) DO UPDATE SET next_offset=EXCLUDED.next_offset,status='in-progress',revision=EXCLUDED.revision`,\n [mapping.targetKey, importKey, mapping.mappingId, input.manifest.contentId, input.manifest.datasetVersion, revision, offset]);\n return { importKey, revision, offset };\n}\n\nexport async function checkpoint(client: QueryClient, mapping: ResolvedMapping, state: ImportState, offset: number, complete: boolean) {\n await client.query(`UPDATE ${mapping.ledgerSql} SET next_offset=$1,status=$2 WHERE target_key=$3 AND import_key=$4`, [offset, complete ? 'complete' : 'in-progress', mapping.targetKey, state.importKey]);\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Entity } from '../types.ts';
|
|
2
|
+
import type { SeedMapping } from './types.ts';
|
|
3
|
+
export declare const fields: readonly ["id", "entityType", "name", "countryId", "parentId", "codes", "names", "level", "coordinates", "boundary"];
|
|
4
|
+
export declare const defaults: Record<keyof Entity, string>;
|
|
5
|
+
export declare function identifier(value: string): string;
|
|
6
|
+
export declare function stable(value: unknown): string;
|
|
7
|
+
export declare const hash: (value: unknown) => string;
|
|
8
|
+
export declare function resolveMapping(mapping?: SeedMapping): {
|
|
9
|
+
schema: string;
|
|
10
|
+
table: string;
|
|
11
|
+
columns: {
|
|
12
|
+
name: string;
|
|
13
|
+
coordinates: string;
|
|
14
|
+
id: string;
|
|
15
|
+
entityType: string;
|
|
16
|
+
countryId: string;
|
|
17
|
+
parentId: string;
|
|
18
|
+
codes: string;
|
|
19
|
+
names: string;
|
|
20
|
+
level: string;
|
|
21
|
+
boundary: string;
|
|
22
|
+
};
|
|
23
|
+
localIdColumn: string | undefined;
|
|
24
|
+
tableSql: string;
|
|
25
|
+
ledgerSql: string;
|
|
26
|
+
targetKey: string;
|
|
27
|
+
mappingId: string;
|
|
28
|
+
};
|
|
29
|
+
export type ResolvedMapping = ReturnType<typeof resolveMapping>;
|
|
30
|
+
export declare function entityFields(entity: Entity, coordinates: boolean, boundaries: boolean): (keyof Entity)[];
|
|
31
|
+
export declare function schemaSql(mapping: ResolvedMapping): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const fields = ['id', 'entityType', 'name', 'countryId', 'parentId', 'codes', 'names', 'level', 'coordinates', 'boundary'];
|
|
3
|
+
export const defaults = {
|
|
4
|
+
id: 'id', entityType: 'entity_type', name: 'name', countryId: 'country_id', parentId: 'parent_id',
|
|
5
|
+
codes: 'codes', names: 'names', level: 'level', coordinates: 'coordinates', boundary: 'boundary',
|
|
6
|
+
};
|
|
7
|
+
export function identifier(value) {
|
|
8
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value) || Buffer.byteLength(value) > 63)
|
|
9
|
+
throw new Error(`Invalid SQL identifier: ${value}`);
|
|
10
|
+
return `"${value}"`;
|
|
11
|
+
}
|
|
12
|
+
export function stable(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(stable).join(',')}]`;
|
|
15
|
+
if (value && typeof value === 'object')
|
|
16
|
+
return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
|
|
17
|
+
return JSON.stringify(value) ?? 'null';
|
|
18
|
+
}
|
|
19
|
+
export const hash = (value) => createHash('sha256').update(stable(value)).digest('hex');
|
|
20
|
+
export function resolveMapping(mapping = {}) {
|
|
21
|
+
const schema = mapping.schema ?? 'public';
|
|
22
|
+
const table = mapping.table ?? 'georeference_entities';
|
|
23
|
+
const columns = { ...defaults, ...mapping.columns };
|
|
24
|
+
const columnValues = Object.values(columns);
|
|
25
|
+
if (new Set(columnValues).size !== columnValues.length)
|
|
26
|
+
throw new Error('Mapping columns must be distinct');
|
|
27
|
+
for (const name of [schema, table, ...columnValues, ...(mapping.localIdColumn ? [mapping.localIdColumn] : [])])
|
|
28
|
+
identifier(name);
|
|
29
|
+
if (mapping.localIdColumn && columnValues.includes(mapping.localIdColumn))
|
|
30
|
+
throw new Error('Local primary key must be separate from the stable identifier and managed columns');
|
|
31
|
+
if (table === '_georeference_imports')
|
|
32
|
+
throw new Error('Target table cannot be the import ledger');
|
|
33
|
+
return {
|
|
34
|
+
schema, table, columns, localIdColumn: mapping.localIdColumn,
|
|
35
|
+
tableSql: `${identifier(schema)}.${identifier(table)}`,
|
|
36
|
+
ledgerSql: `${identifier(schema)}."_georeference_imports"`,
|
|
37
|
+
targetKey: hash({ schema, table }),
|
|
38
|
+
mappingId: hash({ schema, table, columns, localIdColumn: mapping.localIdColumn ?? null }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function entityFields(entity, coordinates, boundaries) {
|
|
42
|
+
return fields.filter(field => field in entity && (field !== 'coordinates' || coordinates) && (field !== 'boundary' || boundaries));
|
|
43
|
+
}
|
|
44
|
+
export function schemaSql(mapping) {
|
|
45
|
+
const c = (field) => identifier(mapping.columns[field]);
|
|
46
|
+
return `CREATE TABLE ${mapping.tableSql} (
|
|
47
|
+
${c('id')} TEXT PRIMARY KEY, ${c('entityType')} TEXT NOT NULL,
|
|
48
|
+
${c('name')} TEXT NOT NULL, ${c('countryId')} TEXT NOT NULL,
|
|
49
|
+
${c('parentId')} TEXT REFERENCES ${mapping.tableSql}(${c('id')}) DEFERRABLE INITIALLY DEFERRED,
|
|
50
|
+
${c('codes')} JSONB NOT NULL DEFAULT '{}', ${c('names')} JSONB NOT NULL DEFAULT '{}',
|
|
51
|
+
${c('level')} INTEGER, ${c('coordinates')} JSONB, ${c('boundary')} JSONB
|
|
52
|
+
)`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=mapping.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapping.js","sourceRoot":"","sources":["../../src/seeding/mapping.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport type { Entity } from '../types.ts';\nimport type { SeedMapping } from './types.ts';\n\nexport const fields = ['id', 'entityType', 'name', 'countryId', 'parentId', 'codes', 'names', 'level', 'coordinates', 'boundary'] as const;\nexport const defaults: Record<keyof Entity, string> = {\n id: 'id', entityType: 'entity_type', name: 'name', countryId: 'country_id', parentId: 'parent_id',\n codes: 'codes', names: 'names', level: 'level', coordinates: 'coordinates', boundary: 'boundary',\n};\nexport function identifier(value: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value) || Buffer.byteLength(value) > 63) throw new Error(`Invalid SQL identifier: ${value}`);\n return `\"${value}\"`;\n}\nexport function stable(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;\n if (value && typeof value === 'object') return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;\n return JSON.stringify(value) ?? 'null';\n}\nexport const hash = (value: unknown) => createHash('sha256').update(stable(value)).digest('hex');\n\nexport function resolveMapping(mapping: SeedMapping = {}) {\n const schema = mapping.schema ?? 'public';\n const table = mapping.table ?? 'georeference_entities';\n const columns = { ...defaults, ...mapping.columns };\n const columnValues = Object.values(columns);\n if (new Set(columnValues).size !== columnValues.length) throw new Error('Mapping columns must be distinct');\n for (const name of [schema, table, ...columnValues, ...(mapping.localIdColumn ? [mapping.localIdColumn] : [])]) identifier(name);\n if (mapping.localIdColumn && columnValues.includes(mapping.localIdColumn)) throw new Error('Local primary key must be separate from the stable identifier and managed columns');\n if (table === '_georeference_imports') throw new Error('Target table cannot be the import ledger');\n return {\n schema, table, columns, localIdColumn: mapping.localIdColumn,\n tableSql: `${identifier(schema)}.${identifier(table)}`,\n ledgerSql: `${identifier(schema)}.\"_georeference_imports\"`,\n targetKey: hash({ schema, table }),\n mappingId: hash({ schema, table, columns, localIdColumn: mapping.localIdColumn ?? null }),\n };\n}\nexport type ResolvedMapping = ReturnType<typeof resolveMapping>;\n\nexport function entityFields(entity: Entity, coordinates: boolean, boundaries: boolean): (keyof Entity)[] {\n return fields.filter(field => field in entity && (field !== 'coordinates' || coordinates) && (field !== 'boundary' || boundaries));\n}\n\nexport function schemaSql(mapping: ResolvedMapping): string {\n const c = (field: keyof Entity) => identifier(mapping.columns[field]);\n return `CREATE TABLE ${mapping.tableSql} (\n ${c('id')} TEXT PRIMARY KEY, ${c('entityType')} TEXT NOT NULL,\n ${c('name')} TEXT NOT NULL, ${c('countryId')} TEXT NOT NULL,\n ${c('parentId')} TEXT REFERENCES ${mapping.tableSql}(${c('id')}) DEFERRABLE INITIALLY DEFERRED,\n ${c('codes')} JSONB NOT NULL DEFAULT '{}', ${c('names')} JSONB NOT NULL DEFAULT '{}',\n ${c('level')} INTEGER, ${c('coordinates')} JSONB, ${c('boundary')} JSONB\n )`;\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Entity } from '../types.ts';
|
|
2
|
+
import type { QueryClient, SeedInput, SeedOptions, SeedReport } from './types.ts';
|
|
3
|
+
import type { ResolvedMapping } from './mapping.ts';
|
|
4
|
+
export declare function targetExists(client: QueryClient, mapping: ResolvedMapping): Promise<boolean>;
|
|
5
|
+
export declare function validateInput(input: SeedInput, options: SeedOptions): Entity[];
|
|
6
|
+
export declare function existingRows(client: QueryClient, mapping: ResolvedMapping, entities: Entity[], input: SeedInput, exists: boolean): Promise<Map<string, Record<string, unknown>>>;
|
|
7
|
+
export declare function validateReferences(entities: Entity[], existing: Map<string, Record<string, unknown>>, options: SeedOptions): void;
|
|
8
|
+
export declare function localIds(existing: Map<string, Record<string, unknown>>): Map<string, unknown>;
|
|
9
|
+
export declare function valuesFor(entity: Entity, fields: (keyof Entity)[], mapping: ResolvedMapping, ids: Map<string, unknown>): unknown[];
|
|
10
|
+
export declare function summarize(input: SeedInput, entities: Entity[], existing: Map<string, Record<string, unknown>>, mapping: ResolvedMapping, options: SeedOptions): SeedReport;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { entityFields, fields, identifier, stable } from "./mapping.js";
|
|
2
|
+
export async function targetExists(client, mapping) {
|
|
3
|
+
const result = await client.query('SELECT to_regclass($1) AS relation', [mapping.tableSql]);
|
|
4
|
+
return result.rows[0]?.relation != null;
|
|
5
|
+
}
|
|
6
|
+
export function validateInput(input, options) {
|
|
7
|
+
if (input.manifest.contractVersion !== '1.0')
|
|
8
|
+
throw new Error('Unsupported package contract version');
|
|
9
|
+
if (input.manifest.request.spreadsheetSafe)
|
|
10
|
+
throw new Error('Spreadsheet-safe CSV cannot be used for database seeding');
|
|
11
|
+
if (!input.manifest.datasetVersion || !input.manifest.contentId)
|
|
12
|
+
throw new Error('Package version and content ID are required');
|
|
13
|
+
if (input.entities.length !== input.manifest.count)
|
|
14
|
+
throw new Error('Package entity count does not match the manifest');
|
|
15
|
+
if (!Number.isSafeInteger(options.batchSize ?? 500) || (options.batchSize ?? 500) < 1 || (options.batchSize ?? 500) > 10000)
|
|
16
|
+
throw new Error('batchSize must be an integer from 1 to 10000');
|
|
17
|
+
if (!Number.isSafeInteger(options.revision ?? 0) || (options.revision ?? 0) < 0)
|
|
18
|
+
throw new Error('revision must be a nonnegative safe integer');
|
|
19
|
+
if (options.mode !== undefined && !['upsert', 'insert-only'].includes(options.mode))
|
|
20
|
+
throw new Error('Unsupported seeding mode');
|
|
21
|
+
const byId = new Map();
|
|
22
|
+
for (const entity of input.entities) {
|
|
23
|
+
if (!entity.id || !entity.name || !entity.countryId || !['country', 'subdivision', 'locality'].includes(entity.entityType))
|
|
24
|
+
throw new Error('Invalid canonical entity');
|
|
25
|
+
if (byId.has(entity.id))
|
|
26
|
+
throw new Error(`Duplicate stable identifier: ${entity.id}`);
|
|
27
|
+
if (entity.entityType === 'country' && (entity.parentId !== null || entity.countryId !== entity.id))
|
|
28
|
+
throw new Error(`Invalid country relationships: ${entity.id}`);
|
|
29
|
+
if (entity.parentId === entity.id)
|
|
30
|
+
throw new Error(`Entity is its own parent: ${entity.id}`);
|
|
31
|
+
byId.set(entity.id, entity);
|
|
32
|
+
}
|
|
33
|
+
const ordered = [];
|
|
34
|
+
const visiting = new Set();
|
|
35
|
+
const done = new Set();
|
|
36
|
+
const visit = (entity) => {
|
|
37
|
+
if (done.has(entity.id))
|
|
38
|
+
return;
|
|
39
|
+
if (visiting.has(entity.id))
|
|
40
|
+
throw new Error(`Cycle in parent references: ${entity.id}`);
|
|
41
|
+
visiting.add(entity.id);
|
|
42
|
+
if (entity.countryId !== entity.id && byId.has(entity.countryId))
|
|
43
|
+
visit(byId.get(entity.countryId));
|
|
44
|
+
if (entity.parentId && byId.has(entity.parentId)) {
|
|
45
|
+
const parent = byId.get(entity.parentId);
|
|
46
|
+
if (parent.countryId !== entity.countryId)
|
|
47
|
+
throw new Error(`Parent country mismatch: ${entity.id}`);
|
|
48
|
+
visit(parent);
|
|
49
|
+
}
|
|
50
|
+
visiting.delete(entity.id);
|
|
51
|
+
done.add(entity.id);
|
|
52
|
+
ordered.push(entity);
|
|
53
|
+
};
|
|
54
|
+
for (const entity of [...byId.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
55
|
+
visit(entity);
|
|
56
|
+
return ordered;
|
|
57
|
+
}
|
|
58
|
+
export async function existingRows(client, mapping, entities, input, exists) {
|
|
59
|
+
const result = new Map();
|
|
60
|
+
if (!exists)
|
|
61
|
+
return result;
|
|
62
|
+
const unique = (await client.query(`SELECT EXISTS (
|
|
63
|
+
SELECT 1 FROM pg_index i JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attname=$2
|
|
64
|
+
WHERE i.indrelid=$1::regclass AND i.indisunique AND i.indisvalid AND i.indisready
|
|
65
|
+
AND i.indpred IS NULL AND i.indexprs IS NULL AND i.indnkeyatts=1 AND i.indkey[0]=a.attnum
|
|
66
|
+
) AS stable_unique`, [mapping.tableSql, mapping.columns.id])).rows[0]?.stable_unique;
|
|
67
|
+
if (unique !== true)
|
|
68
|
+
throw new Error('Stable identifier column requires a single-column UNIQUE or PRIMARY KEY');
|
|
69
|
+
const available = new Set((await client.query('SELECT column_name FROM information_schema.columns WHERE table_schema=$1 AND table_name=$2', [mapping.schema, mapping.table])).rows.map(row => row.column_name));
|
|
70
|
+
const needed = new Set(['id', 'entityType', 'name', 'countryId', 'parentId']);
|
|
71
|
+
for (const entity of entities)
|
|
72
|
+
for (const field of entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false))
|
|
73
|
+
needed.add(field);
|
|
74
|
+
for (const field of needed)
|
|
75
|
+
if (!available.has(mapping.columns[field]))
|
|
76
|
+
throw new Error(`Missing target column: ${mapping.columns[field]}`);
|
|
77
|
+
if (mapping.localIdColumn && !available.has(mapping.localIdColumn))
|
|
78
|
+
throw new Error(`Missing local key column: ${mapping.localIdColumn}`);
|
|
79
|
+
const selects = fields.filter(field => needed.has(field)).map(field => `${identifier(mapping.columns[field])} AS ${identifier(field)}`);
|
|
80
|
+
if (mapping.localIdColumn)
|
|
81
|
+
selects.push(`${identifier(mapping.localIdColumn)} AS "_localId"`);
|
|
82
|
+
const ids = [...new Set(entities.flatMap(entity => [entity.id, entity.countryId, ...(entity.parentId ? [entity.parentId] : [])]))];
|
|
83
|
+
for (let offset = 0; offset < ids.length; offset += 1000) {
|
|
84
|
+
const loaded = await client.query(`SELECT ${selects.join(',')} FROM ${mapping.tableSql} WHERE ${identifier(mapping.columns.id)} = ANY($1::text[])`, [ids.slice(offset, offset + 1000)]);
|
|
85
|
+
for (const row of loaded.rows)
|
|
86
|
+
result.set(String(row.id), row);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
export function validateReferences(entities, existing, options) {
|
|
91
|
+
const byId = new Map(entities.map(entity => [entity.id, entity]));
|
|
92
|
+
const effective = (id) => options.mode === 'insert-only' && existing.has(id) ? existing.get(id) : byId.get(id) ?? existing.get(id);
|
|
93
|
+
for (const entity of entities) {
|
|
94
|
+
// Existing rows are deliberately not modified in insert-only mode. Dependent new
|
|
95
|
+
// rows must therefore be checked against those actual rows, not skipped updates.
|
|
96
|
+
if (options.mode === 'insert-only' && existing.has(entity.id))
|
|
97
|
+
continue;
|
|
98
|
+
for (const id of [entity.countryId, ...(entity.parentId ? [entity.parentId] : [])]) {
|
|
99
|
+
if (!byId.has(id) && !existing.has(id))
|
|
100
|
+
throw new Error(`Unresolved reference ${id} from ${entity.id}`);
|
|
101
|
+
}
|
|
102
|
+
const country = effective(entity.countryId);
|
|
103
|
+
if (country?.entityType !== 'country')
|
|
104
|
+
throw new Error(`Country reference is not a country: ${entity.countryId}`);
|
|
105
|
+
const parent = entity.parentId ? effective(entity.parentId) : undefined;
|
|
106
|
+
if (parent) {
|
|
107
|
+
if (parent.entityType === 'locality')
|
|
108
|
+
throw new Error(`Administrative parent cannot be a locality: ${entity.parentId}`);
|
|
109
|
+
const expectedCountry = '_localId' in parent ? existing.get(entity.countryId)?._localId : entity.countryId;
|
|
110
|
+
if (expectedCountry == null || stable(parent.countryId) !== stable(expectedCountry))
|
|
111
|
+
throw new Error(`Parent country mismatch: ${entity.id}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function localIds(existing) {
|
|
116
|
+
return new Map([...existing].map(([id, row]) => [id, row._localId]));
|
|
117
|
+
}
|
|
118
|
+
export function valuesFor(entity, fields, mapping, ids) {
|
|
119
|
+
return fields.map(field => {
|
|
120
|
+
const value = entity[field];
|
|
121
|
+
if (mapping.localIdColumn && (field === 'countryId' || field === 'parentId') && value !== null)
|
|
122
|
+
return ids.get(String(value)) ?? { unresolvedReference: String(value) };
|
|
123
|
+
return value ?? null;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export function summarize(input, entities, existing, mapping, options) {
|
|
127
|
+
const report = { dryRun: options.dryRun ?? true, inserted: 0, updated: 0, unchanged: 0, skipped: 0, conflicts: [], resumedBatches: 0, completedBatches: 0, total: entities.length, revision: options.revision ?? 0 };
|
|
128
|
+
const ids = localIds(existing);
|
|
129
|
+
for (const entity of entities) {
|
|
130
|
+
const current = existing.get(entity.id);
|
|
131
|
+
if (!current) {
|
|
132
|
+
report.inserted++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const selected = entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false);
|
|
136
|
+
const incoming = valuesFor(entity, selected, mapping, ids);
|
|
137
|
+
const changed = selected.some((field, index) => stable(current[field]) !== stable(incoming[index]));
|
|
138
|
+
if (!changed)
|
|
139
|
+
report.unchanged++;
|
|
140
|
+
else if (options.mode === 'insert-only') {
|
|
141
|
+
report.skipped++;
|
|
142
|
+
report.conflicts.push(entity.id);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
report.updated++;
|
|
146
|
+
}
|
|
147
|
+
return report;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../src/seeding/preflight.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Entity } from '../types.ts';\nimport type { QueryClient, SeedInput, SeedOptions, SeedReport } from './types.ts';\nimport { entityFields, fields, identifier, stable } from './mapping.ts';\nimport type { ResolvedMapping } from './mapping.ts';\n\nexport async function targetExists(client: QueryClient, mapping: ResolvedMapping): Promise<boolean> {\n const result = await client.query('SELECT to_regclass($1) AS relation', [mapping.tableSql]);\n return result.rows[0]?.relation != null;\n}\n\nexport function validateInput(input: SeedInput, options: SeedOptions): Entity[] {\n if (input.manifest.contractVersion !== '1.0') throw new Error('Unsupported package contract version');\n if (input.manifest.request.spreadsheetSafe) throw new Error('Spreadsheet-safe CSV cannot be used for database seeding');\n if (!input.manifest.datasetVersion || !input.manifest.contentId) throw new Error('Package version and content ID are required');\n if (input.entities.length !== input.manifest.count) throw new Error('Package entity count does not match the manifest');\n if (!Number.isSafeInteger(options.batchSize ?? 500) || (options.batchSize ?? 500) < 1 || (options.batchSize ?? 500) > 10000) throw new Error('batchSize must be an integer from 1 to 10000');\n if (!Number.isSafeInteger(options.revision ?? 0) || (options.revision ?? 0) < 0) throw new Error('revision must be a nonnegative safe integer');\n if (options.mode !== undefined && !['upsert', 'insert-only'].includes(options.mode)) throw new Error('Unsupported seeding mode');\n const byId = new Map<string, Entity>();\n for (const entity of input.entities) {\n if (!entity.id || !entity.name || !entity.countryId || !['country', 'subdivision', 'locality'].includes(entity.entityType)) throw new Error('Invalid canonical entity');\n if (byId.has(entity.id)) throw new Error(`Duplicate stable identifier: ${entity.id}`);\n if (entity.entityType === 'country' && (entity.parentId !== null || entity.countryId !== entity.id)) throw new Error(`Invalid country relationships: ${entity.id}`);\n if (entity.parentId === entity.id) throw new Error(`Entity is its own parent: ${entity.id}`);\n byId.set(entity.id, entity);\n }\n const ordered: Entity[] = [];\n const visiting = new Set<string>();\n const done = new Set<string>();\n const visit = (entity: Entity) => {\n if (done.has(entity.id)) return;\n if (visiting.has(entity.id)) throw new Error(`Cycle in parent references: ${entity.id}`);\n visiting.add(entity.id);\n if (entity.countryId !== entity.id && byId.has(entity.countryId)) visit(byId.get(entity.countryId)!);\n if (entity.parentId && byId.has(entity.parentId)) {\n const parent = byId.get(entity.parentId)!;\n if (parent.countryId !== entity.countryId) throw new Error(`Parent country mismatch: ${entity.id}`);\n visit(parent);\n }\n visiting.delete(entity.id);\n done.add(entity.id);\n ordered.push(entity);\n };\n for (const entity of [...byId.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) visit(entity);\n return ordered;\n}\n\nexport async function existingRows(client: QueryClient, mapping: ResolvedMapping, entities: Entity[], input: SeedInput, exists: boolean) {\n const result = new Map<string, Record<string, unknown>>();\n if (!exists) return result;\n const unique = (await client.query(`SELECT EXISTS (\n SELECT 1 FROM pg_index i JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attname=$2\n WHERE i.indrelid=$1::regclass AND i.indisunique AND i.indisvalid AND i.indisready\n AND i.indpred IS NULL AND i.indexprs IS NULL AND i.indnkeyatts=1 AND i.indkey[0]=a.attnum\n ) AS stable_unique`, [mapping.tableSql, mapping.columns.id])).rows[0]?.stable_unique;\n if (unique !== true) throw new Error('Stable identifier column requires a single-column UNIQUE or PRIMARY KEY');\n const available = new Set((await client.query('SELECT column_name FROM information_schema.columns WHERE table_schema=$1 AND table_name=$2', [mapping.schema, mapping.table])).rows.map(row => row.column_name));\n const needed = new Set<keyof Entity>(['id', 'entityType', 'name', 'countryId', 'parentId']);\n for (const entity of entities) for (const field of entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false)) needed.add(field);\n for (const field of needed) if (!available.has(mapping.columns[field])) throw new Error(`Missing target column: ${mapping.columns[field]}`);\n if (mapping.localIdColumn && !available.has(mapping.localIdColumn)) throw new Error(`Missing local key column: ${mapping.localIdColumn}`);\n const selects = fields.filter(field => needed.has(field)).map(field => `${identifier(mapping.columns[field])} AS ${identifier(field)}`);\n if (mapping.localIdColumn) selects.push(`${identifier(mapping.localIdColumn)} AS \"_localId\"`);\n const ids = [...new Set(entities.flatMap(entity => [entity.id, entity.countryId, ...(entity.parentId ? [entity.parentId] : [])]))];\n for (let offset = 0; offset < ids.length; offset += 1000) {\n const loaded = await client.query(`SELECT ${selects.join(',')} FROM ${mapping.tableSql} WHERE ${identifier(mapping.columns.id)} = ANY($1::text[])`, [ids.slice(offset, offset + 1000)]);\n for (const row of loaded.rows) result.set(String(row.id), row);\n }\n return result;\n}\n\nexport function validateReferences(entities: Entity[], existing: Map<string, Record<string, unknown>>, options: SeedOptions) {\n const byId = new Map(entities.map(entity => [entity.id, entity]));\n const effective = (id: string) => options.mode === 'insert-only' && existing.has(id) ? existing.get(id) : byId.get(id) ?? existing.get(id);\n for (const entity of entities) {\n // Existing rows are deliberately not modified in insert-only mode. Dependent new\n // rows must therefore be checked against those actual rows, not skipped updates.\n if (options.mode === 'insert-only' && existing.has(entity.id)) continue;\n for (const id of [entity.countryId, ...(entity.parentId ? [entity.parentId] : [])]) {\n if (!byId.has(id) && !existing.has(id)) throw new Error(`Unresolved reference ${id} from ${entity.id}`);\n }\n const country = effective(entity.countryId);\n if (country?.entityType !== 'country') throw new Error(`Country reference is not a country: ${entity.countryId}`);\n const parent = entity.parentId ? effective(entity.parentId) : undefined;\n if (parent) {\n if (parent.entityType === 'locality') throw new Error(`Administrative parent cannot be a locality: ${entity.parentId}`);\n const expectedCountry = '_localId' in parent ? existing.get(entity.countryId)?._localId : entity.countryId;\n if (expectedCountry == null || stable(parent.countryId) !== stable(expectedCountry)) throw new Error(`Parent country mismatch: ${entity.id}`);\n }\n }\n}\n\nexport function localIds(existing: Map<string, Record<string, unknown>>): Map<string, unknown> {\n return new Map([...existing].map(([id, row]) => [id, row._localId]));\n}\n\nexport function valuesFor(entity: Entity, fields: (keyof Entity)[], mapping: ResolvedMapping, ids: Map<string, unknown>): unknown[] {\n return fields.map(field => {\n const value = entity[field];\n if (mapping.localIdColumn && (field === 'countryId' || field === 'parentId') && value !== null) return ids.get(String(value)) ?? { unresolvedReference: String(value) };\n return value ?? null;\n });\n}\n\nexport function summarize(input: SeedInput, entities: Entity[], existing: Map<string, Record<string, unknown>>, mapping: ResolvedMapping, options: SeedOptions): SeedReport {\n const report: SeedReport = { dryRun: options.dryRun ?? true, inserted: 0, updated: 0, unchanged: 0, skipped: 0, conflicts: [], resumedBatches: 0, completedBatches: 0, total: entities.length, revision: options.revision ?? 0 };\n const ids = localIds(existing);\n for (const entity of entities) {\n const current = existing.get(entity.id);\n if (!current) { report.inserted++; continue; }\n const selected = entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false);\n const incoming = valuesFor(entity, selected, mapping, ids);\n const changed = selected.some((field, index) => stable(current[field]) !== stable(incoming[index]));\n if (!changed) report.unchanged++;\n else if (options.mode === 'insert-only') { report.skipped++; report.conflicts.push(entity.id); }\n else report.updated++;\n }\n return report;\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Entity } from '../types.ts';
|
|
2
|
+
import type { ResolvedMapping } from './mapping.ts';
|
|
3
|
+
import type { QueryClient, SeedOptions } from './types.ts';
|
|
4
|
+
/** Partial updates must not invalidate descendants intentionally absent from the package. */
|
|
5
|
+
export declare function validateRetainedChildren(client: QueryClient, mapping: ResolvedMapping, entities: Entity[], existing: Map<string, Record<string, unknown>>, options: SeedOptions): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { identifier, stable } from "./mapping.js";
|
|
2
|
+
/** Partial updates must not invalidate descendants intentionally absent from the package. */
|
|
3
|
+
export async function validateRetainedChildren(client, mapping, entities, existing, options) {
|
|
4
|
+
if (options.mode === 'insert-only')
|
|
5
|
+
return;
|
|
6
|
+
const changed = new Map();
|
|
7
|
+
for (const entity of entities) {
|
|
8
|
+
const current = existing.get(entity.id);
|
|
9
|
+
if (!current)
|
|
10
|
+
continue;
|
|
11
|
+
const country = mapping.localIdColumn ? existing.get(entity.countryId)?._localId : entity.countryId;
|
|
12
|
+
if (current.entityType !== entity.entityType || stable(current.countryId) !== stable(country)) {
|
|
13
|
+
changed.set(String(mapping.localIdColumn ? current._localId : entity.id), { entity, country });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const ids = [...changed.keys()];
|
|
17
|
+
if (ids.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
const included = entities.map(entity => entity.id);
|
|
20
|
+
const c = (field) => identifier(mapping.columns[field]);
|
|
21
|
+
for (let offset = 0; offset < ids.length; offset += 1000) {
|
|
22
|
+
const children = (await client.query(`SELECT ${c('id')} AS id,${c('countryId')} AS country,${c('parentId')} AS parent
|
|
23
|
+
FROM ${mapping.tableSql} WHERE ${c('parentId')}::text = ANY($1::text[]) AND ${c('id')} <> ALL($2::text[])`, [ids.slice(offset, offset + 1000), included])).rows;
|
|
24
|
+
for (const child of children) {
|
|
25
|
+
const parent = changed.get(String(child.parent));
|
|
26
|
+
if (parent.entity.entityType === 'locality' || parent.country == null || stable(child.country) !== stable(parent.country)) {
|
|
27
|
+
throw new Error(`Update would invalidate retained child ${String(child.id)}; include and update the affected subtree`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=retained.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retained.js","sourceRoot":"","sources":["../../src/seeding/retained.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAIlD,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,MAAmB,EAAE,OAAwB,EAAE,QAAkB,EAAE,QAA8C,EAAE,OAAoB;IACpL,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa;QAAE,OAAO;IAC3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgD,CAAC;IACxE,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;QACpG,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9F,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC7B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,CAAC,KAAmB,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;QACzD,MAAM,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC;aACjG,OAAO,CAAC,QAAQ,UAAU,CAAC,CAAC,UAAU,CAAC,gCAAgC,CAAC,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAClK,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAE,CAAC;YAClD,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,KAAK,UAAU,IAAI,MAAM,CAAC,OAAO,IAAI,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1H,MAAM,IAAI,KAAK,CAAC,0CAA0C,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,2CAA2C,CAAC,CAAC;YACzH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import type { Entity } from '../types.ts';\nimport { identifier, stable } from './mapping.ts';\nimport type { ResolvedMapping } from './mapping.ts';\nimport type { QueryClient, SeedOptions } from './types.ts';\n\n/** Partial updates must not invalidate descendants intentionally absent from the package. */\nexport async function validateRetainedChildren(client: QueryClient, mapping: ResolvedMapping, entities: Entity[], existing: Map<string, Record<string, unknown>>, options: SeedOptions): Promise<void> {\n if (options.mode === 'insert-only') return;\n const changed = new Map<string, { entity: Entity; country: unknown }>();\n for (const entity of entities) {\n const current = existing.get(entity.id);\n if (!current) continue;\n const country = mapping.localIdColumn ? existing.get(entity.countryId)?._localId : entity.countryId;\n if (current.entityType !== entity.entityType || stable(current.countryId) !== stable(country)) {\n changed.set(String(mapping.localIdColumn ? current._localId : entity.id), { entity, country });\n }\n }\n const ids = [...changed.keys()];\n if (ids.length === 0) return;\n const included = entities.map(entity => entity.id);\n const c = (field: keyof Entity) => identifier(mapping.columns[field]);\n for (let offset = 0; offset < ids.length; offset += 1000) {\n const children = (await client.query(`SELECT ${c('id')} AS id,${c('countryId')} AS country,${c('parentId')} AS parent\n FROM ${mapping.tableSql} WHERE ${c('parentId')}::text = ANY($1::text[]) AND ${c('id')} <> ALL($2::text[])`, [ids.slice(offset, offset + 1000), included])).rows;\n for (const child of children) {\n const parent = changed.get(String(child.parent))!;\n if (parent.entity.entityType === 'locality' || parent.country == null || stable(child.country) !== stable(parent.country)) {\n throw new Error(`Update would invalidate retained child ${String(child.id)}; include and update the affected subtree`);\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Entity, ExportManifest } from '../types.ts';
|
|
2
|
+
export interface QueryClient {
|
|
3
|
+
/** Use a dedicated connection, not a pool: transactions and advisory locks are session-bound. */
|
|
4
|
+
query(text: string, values?: unknown[]): Promise<{
|
|
5
|
+
rows: Record<string, unknown>[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
export interface SeedMapping {
|
|
9
|
+
schema?: string;
|
|
10
|
+
table?: string;
|
|
11
|
+
columns?: Partial<Record<keyof Entity, string>>;
|
|
12
|
+
/** Existing serial/identity primary key; references are resolved to these local keys. */
|
|
13
|
+
localIdColumn?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SeedOptions {
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
createSchema?: boolean;
|
|
18
|
+
mode?: 'insert-only' | 'upsert';
|
|
19
|
+
batchSize?: number;
|
|
20
|
+
/** Monotonic import revision, independent of the opaque dataset version label. */
|
|
21
|
+
revision?: number;
|
|
22
|
+
allowDowngrade?: boolean;
|
|
23
|
+
mapping?: SeedMapping;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
export interface SeedInput {
|
|
27
|
+
manifest: ExportManifest;
|
|
28
|
+
entities: Entity[];
|
|
29
|
+
}
|
|
30
|
+
export interface SeedReport {
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
inserted: number;
|
|
33
|
+
updated: number;
|
|
34
|
+
unchanged: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
conflicts: string[];
|
|
37
|
+
resumedBatches: number;
|
|
38
|
+
completedBatches: number;
|
|
39
|
+
total: number;
|
|
40
|
+
revision: number;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/seeding/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Entity, ExportManifest } from '../types.ts';\n\nexport interface QueryClient {\n /** Use a dedicated connection, not a pool: transactions and advisory locks are session-bound. */\n query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;\n}\nexport interface SeedMapping {\n schema?: string;\n table?: string;\n columns?: Partial<Record<keyof Entity, string>>;\n /** Existing serial/identity primary key; references are resolved to these local keys. */\n localIdColumn?: string;\n}\nexport interface SeedOptions {\n dryRun?: boolean;\n createSchema?: boolean;\n mode?: 'insert-only' | 'upsert';\n batchSize?: number;\n /** Monotonic import revision, independent of the opaque dataset version label. */\n revision?: number;\n allowDowngrade?: boolean;\n mapping?: SeedMapping;\n signal?: AbortSignal;\n}\nexport interface SeedInput { manifest: ExportManifest; entities: Entity[] }\nexport interface SeedReport {\n dryRun: boolean;\n inserted: number;\n updated: number;\n unchanged: number;\n skipped: number;\n conflicts: string[];\n resumedBatches: number;\n completedBatches: number;\n total: number;\n revision: number;\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Entity } from '../types.ts';
|
|
2
|
+
import type { ResolvedMapping } from './mapping.ts';
|
|
3
|
+
import type { QueryClient, SeedInput, SeedOptions } from './types.ts';
|
|
4
|
+
export declare function writeEntity(client: QueryClient, mapping: ResolvedMapping, entity: Entity, input: SeedInput, options: SeedOptions, existing: Map<string, Record<string, unknown>>, ids: Map<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { entityFields, identifier, stable } from "./mapping.js";
|
|
2
|
+
import { valuesFor } from "./preflight.js";
|
|
3
|
+
export async function writeEntity(client, mapping, entity, input, options, existing, ids) {
|
|
4
|
+
const current = existing.get(entity.id);
|
|
5
|
+
const fields = entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false);
|
|
6
|
+
if (!current && mapping.localIdColumn) {
|
|
7
|
+
const reserved = (await client.query('SELECT nextval(pg_get_serial_sequence($1,$2)) AS local_id', [mapping.tableSql, mapping.localIdColumn])).rows[0]?.local_id;
|
|
8
|
+
if (reserved == null)
|
|
9
|
+
throw new Error('Local primary key mapping requires a serial/identity sequence');
|
|
10
|
+
ids.set(entity.id, reserved);
|
|
11
|
+
}
|
|
12
|
+
const values = valuesFor(entity, fields, mapping, ids);
|
|
13
|
+
if (mapping.localIdColumn && [entity.countryId, entity.parentId].some(id => id !== null && ids.get(id) == null))
|
|
14
|
+
throw new Error(`Unresolved local reference for ${entity.id}`);
|
|
15
|
+
if (current) {
|
|
16
|
+
if (options.mode === 'insert-only' || fields.every((field, index) => stable(current[field]) === stable(values[index])))
|
|
17
|
+
return;
|
|
18
|
+
const changes = fields.filter(field => field !== 'id');
|
|
19
|
+
const updateValues = changes.map(field => values[fields.indexOf(field)]);
|
|
20
|
+
await client.query(`UPDATE ${mapping.tableSql} SET ${changes.map((field, index) => `${identifier(mapping.columns[field])}=$${index + 1}`).join(',')} WHERE ${identifier(mapping.columns.id)}=$${updateValues.length + 1}`, [...updateValues, entity.id]);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const columns = fields.map(field => identifier(mapping.columns[field]));
|
|
24
|
+
if (mapping.localIdColumn) {
|
|
25
|
+
columns.push(identifier(mapping.localIdColumn));
|
|
26
|
+
values.push(ids.get(entity.id));
|
|
27
|
+
}
|
|
28
|
+
const override = mapping.localIdColumn ? ' OVERRIDING SYSTEM VALUE' : '';
|
|
29
|
+
await client.query(`INSERT INTO ${mapping.tableSql} (${columns.join(',')})${override} VALUES (${values.map((_, index) => `$${index + 1}`).join(',')})`, values);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/seeding/write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Entity } from '../types.ts';\nimport { entityFields, identifier, stable } from './mapping.ts';\nimport type { ResolvedMapping } from './mapping.ts';\nimport { valuesFor } from './preflight.ts';\nimport type { QueryClient, SeedInput, SeedOptions } from './types.ts';\n\nexport async function writeEntity(client: QueryClient, mapping: ResolvedMapping, entity: Entity, input: SeedInput, options: SeedOptions, existing: Map<string, Record<string, unknown>>, ids: Map<string, unknown>) {\n const current = existing.get(entity.id);\n const fields = entityFields(entity, input.manifest.request.coordinates ?? false, input.manifest.request.boundaries ?? false);\n if (!current && mapping.localIdColumn) {\n const reserved = (await client.query('SELECT nextval(pg_get_serial_sequence($1,$2)) AS local_id', [mapping.tableSql, mapping.localIdColumn])).rows[0]?.local_id;\n if (reserved == null) throw new Error('Local primary key mapping requires a serial/identity sequence');\n ids.set(entity.id, reserved);\n }\n const values = valuesFor(entity, fields, mapping, ids);\n if (mapping.localIdColumn && [entity.countryId, entity.parentId].some(id => id !== null && ids.get(id) == null)) throw new Error(`Unresolved local reference for ${entity.id}`);\n if (current) {\n if (options.mode === 'insert-only' || fields.every((field, index) => stable(current[field]) === stable(values[index]))) return;\n const changes = fields.filter(field => field !== 'id');\n const updateValues = changes.map(field => values[fields.indexOf(field)]);\n await client.query(`UPDATE ${mapping.tableSql} SET ${changes.map((field, index) => `${identifier(mapping.columns[field])}=$${index + 1}`).join(',')} WHERE ${identifier(mapping.columns.id)}=$${updateValues.length + 1}`, [...updateValues, entity.id]);\n return;\n }\n const columns = fields.map(field => identifier(mapping.columns[field]));\n if (mapping.localIdColumn) { columns.push(identifier(mapping.localIdColumn)); values.push(ids.get(entity.id)); }\n const override = mapping.localIdColumn ? ' OVERRIDING SYSTEM VALUE' : '';\n await client.query(`INSERT INTO ${mapping.tableSql} (${columns.join(',')})${override} VALUES (${values.map((_, index) => `$${index + 1}`).join(',')})`, values);\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Entity, SnapshotMetadata, Selection, ExportPlan } from './types.ts';
|
|
2
|
+
export declare function createSnapshot(path: string, metadata: SnapshotMetadata, entities: Iterable<Entity> | AsyncIterable<Entity>): Promise<void>;
|
|
3
|
+
export declare class Snapshot {
|
|
4
|
+
private db;
|
|
5
|
+
private nextPlan;
|
|
6
|
+
private constructor();
|
|
7
|
+
static open(path: string): Snapshot;
|
|
8
|
+
get metadata(): SnapshotMetadata;
|
|
9
|
+
get count(): number;
|
|
10
|
+
get(id: string): Entity | undefined;
|
|
11
|
+
entities(): IterableIterator<Entity>;
|
|
12
|
+
select(selection: Selection): ExportPlan;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|