@tricoteuses/senat 2.23.0 → 3.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.
@@ -1,7 +1,9 @@
1
1
  import assert from "assert";
2
- import { execSync } from "child_process";
2
+ import { execFileSync } from "child_process";
3
3
  import commandLineArgs from "command-line-args";
4
4
  import fs from "fs-extra";
5
+ import { formatWithPrettier, makePgTsGenerator, markAsGenerated, processDatabase } from "kanel";
6
+ import { makeGenerateZodSchemas } from "kanel-zod";
5
7
  import path from "path";
6
8
  import StreamZip from "node-stream-zip";
7
9
  import readline from "readline";
@@ -12,7 +14,7 @@ import config from "../config";
12
14
  import { getChosenDatasets, getEnabledDatasets } from "../datasets";
13
15
  import { assertExistingDirectory, commonOptions } from "./shared/cli_helpers";
14
16
  import { buildIncrementalDatasetImportSql, buildNormalizeStagingSchemaSql } from "./shared/incremental_import_sql";
15
- import { normalizeGeneratedDefinition, prefixedName, senatSchemaName, stagingSchemaName, } from "./shared/prefixed_tables";
17
+ import { buildGeneratedTableManifest, getGeneratedDefinitionPath, getGeneratedTableManifestPath, prefixedName, rawTypesDir, senatSchemaName, stagingSchemaName, stripDatasetPrefix, } from "./shared/prefixed_tables";
16
18
  import { buildEnsureSchemaVersionTableSql, buildIncrementSchemaVersionSql, buildSchemaStructureFingerprintQuery, } from "./shared/schema_version";
17
19
  import { buildExportStagingMetadataStatementsQuery } from "./shared/staging_metadata_sql";
18
20
  import { isCopyFromStdinLine, rewriteLineForStagingImport } from "./shared/staging_import";
@@ -27,7 +29,7 @@ const optionsDefinitions = [
27
29
  },
28
30
  {
29
31
  alias: "c",
30
- help: "create TypeScript interfaces from database schemas into src/raw_types_* directories",
32
+ help: "create TypeScript interfaces and Zod schemas from database tables into src/raw_types",
31
33
  name: "schema",
32
34
  type: Boolean,
33
35
  },
@@ -73,9 +75,6 @@ const stagingServerName = "staging_server";
73
75
  function isIncrementalImport(options) {
74
76
  return options["incremental"] === true;
75
77
  }
76
- function shellQuote(value) {
77
- return `'${value.replace(/'/g, `'"'"'`)}'`;
78
- }
79
78
  function connectionEnv(connection) {
80
79
  return {
81
80
  ...process.env,
@@ -124,7 +123,8 @@ async function runWithRetry(operation, options, retryOptions) {
124
123
  throw error;
125
124
  }
126
125
  if (!options["silent"]) {
127
- console.warn(`${retryOptions.label} hit a transient PostgreSQL lock error on attempt ${attempt}/${retryOptions.attempts}; retrying in ${delayMs}ms...`);
126
+ console.warn(`${retryOptions.label} hit a transient PostgreSQL lock error ` +
127
+ `on attempt ${attempt}/${retryOptions.attempts}; retrying in ${delayMs}ms...`);
128
128
  }
129
129
  await sleep(delayMs);
130
130
  attempt += 1;
@@ -132,18 +132,30 @@ async function runWithRetry(operation, options, retryOptions) {
132
132
  }
133
133
  }
134
134
  }
135
- function buildPsqlCommand(baseArgs, connection, options) {
136
- const sudoPrefix = options["sudo"] ? `sudo -u ${options["sudo"]} ` : "";
137
- return (`${sudoPrefix}psql --quiet ` +
138
- `-h ${shellQuote(connection.host)} ` +
139
- `-p ${shellQuote(String(connection.port))} ` +
140
- `-U ${shellQuote(connection.user)} ` +
141
- `-d ${shellQuote(connection.name)} ` +
142
- baseArgs);
135
+ function buildPsqlInvocation(baseArgs, connection, options) {
136
+ const psqlArgs = [
137
+ "--quiet",
138
+ "-h",
139
+ connection.host,
140
+ "-p",
141
+ String(connection.port),
142
+ "-U",
143
+ connection.user,
144
+ "-d",
145
+ connection.name,
146
+ ...baseArgs,
147
+ ];
148
+ if (!options["sudo"]) {
149
+ return { command: "psql", args: psqlArgs };
150
+ }
151
+ return {
152
+ command: "sudo",
153
+ args: ["-u", options["sudo"], "psql", ...psqlArgs],
154
+ };
143
155
  }
144
156
  function runPsqlFile(sqlFilePath, dataDir, options, connection, stopOnError = true) {
145
- const onErrorFlag = stopOnError ? "-v ON_ERROR_STOP=1 " : "";
146
- execSync(buildPsqlCommand(`${onErrorFlag}-f ${shellQuote(sqlFilePath)}`, connection, options), {
157
+ const { command, args } = buildPsqlInvocation([...(stopOnError ? ["-v", "ON_ERROR_STOP=1"] : []), "-f", sqlFilePath], connection, options);
158
+ execFileSync(command, args, {
147
159
  cwd: dataDir,
148
160
  env: connectionEnv(connection),
149
161
  encoding: "utf-8",
@@ -151,8 +163,9 @@ function runPsqlFile(sqlFilePath, dataDir, options, connection, stopOnError = tr
151
163
  });
152
164
  }
153
165
  function runPsqlCommand(command, dataDir, options, connection, stopOnError = true) {
154
- const onErrorFlag = stopOnError ? "-v ON_ERROR_STOP=1 " : "";
155
- execSync(buildPsqlCommand(`${onErrorFlag}-c ${shellQuote(command)}`, connection, options), {
166
+ const psqlCommand = command;
167
+ const { command: binary, args } = buildPsqlInvocation([...(stopOnError ? ["-v", "ON_ERROR_STOP=1"] : []), "-c", psqlCommand], connection, options);
168
+ execFileSync(binary, args, {
156
169
  cwd: dataDir,
157
170
  env: connectionEnv(connection),
158
171
  encoding: "utf-8",
@@ -160,8 +173,9 @@ function runPsqlCommand(command, dataDir, options, connection, stopOnError = tru
160
173
  });
161
174
  }
162
175
  function runPsqlQuery(command, dataDir, options, connection, stopOnError = true) {
163
- const onErrorFlag = stopOnError ? "-v ON_ERROR_STOP=1 " : "";
164
- return execSync(buildPsqlCommand(`${onErrorFlag}-At -c ${shellQuote(command)}`, connection, options), {
176
+ const psqlCommand = command;
177
+ const { command: binary, args } = buildPsqlInvocation([...(stopOnError ? ["-v", "ON_ERROR_STOP=1"] : []), "-At", "-c", psqlCommand], connection, options);
178
+ return execFileSync(binary, args, {
165
179
  cwd: dataDir,
166
180
  env: connectionEnv(connection),
167
181
  encoding: "utf-8",
@@ -171,7 +185,8 @@ function runPsqlQuery(command, dataDir, options, connection, stopOnError = true)
171
185
  function ensureStagingSchemaHasTables(dataset, dataDir, options, connection) {
172
186
  const stagingSchema = stagingSchemaName(dataset.database);
173
187
  const tableCount = Number.parseInt(runPsqlQuery(`SELECT count(*) FROM pg_tables WHERE schemaname = '${escapeSqlLiteral(stagingSchema)}'`, dataDir, options, connection).trim(), 10);
174
- assert(tableCount > 0, `Staging schema ${stagingSchema} is empty after importing ${dataset.database}. Aborting incremental merge to protect ${senatSchemaName}.`);
188
+ assert(tableCount > 0, `Staging schema ${stagingSchema} is empty after importing ${dataset.database}. ` +
189
+ `Aborting incremental merge to protect ${senatSchemaName}.`);
175
190
  }
176
191
  function ensureStagingDatabase(dataDir, options, runtime) {
177
192
  const maintenanceDb = process.env["PGDATABASE"] || "postgres";
@@ -207,16 +222,27 @@ function ensureDatabaseExists(connection, dataDir, options) {
207
222
  runPsqlCommand(`CREATE DATABASE ${connection.name} WITH OWNER ${connection.user}`, dataDir, options, maintenanceConnection);
208
223
  }
209
224
  function ensureForeignStagingServer(dataDir, options, runtime) {
210
- runPsqlCommand(`CREATE EXTENSION IF NOT EXISTS postgres_fdw`, dataDir, options, runtime.target);
225
+ runPsqlCommand("CREATE EXTENSION IF NOT EXISTS postgres_fdw", dataDir, options, runtime.target);
211
226
  runPsqlCommand(`DROP SERVER IF EXISTS ${stagingServerName} CASCADE`, dataDir, options, runtime.target, false);
212
227
  runPsqlCommand([
213
228
  `CREATE SERVER ${stagingServerName}`,
214
- `FOREIGN DATA WRAPPER postgres_fdw`,
215
- `OPTIONS (host '${escapeSqlLiteral(runtime.staging.host)}', dbname '${escapeSqlLiteral(runtime.staging.name)}', port '${escapeSqlLiteral(String(runtime.staging.port))}')`,
229
+ "FOREIGN DATA WRAPPER postgres_fdw",
230
+ [
231
+ "OPTIONS (",
232
+ `host '${escapeSqlLiteral(runtime.staging.host)}', `,
233
+ `dbname '${escapeSqlLiteral(runtime.staging.name)}', `,
234
+ `port '${escapeSqlLiteral(String(runtime.staging.port))}'`,
235
+ ")",
236
+ ].join(""),
216
237
  ].join(" "), dataDir, options, runtime.target);
217
238
  runPsqlCommand([
218
239
  `CREATE USER MAPPING FOR CURRENT_USER SERVER ${stagingServerName}`,
219
- `OPTIONS (user '${escapeSqlLiteral(runtime.staging.user)}', password '${escapeSqlLiteral(runtime.staging.password)}')`,
240
+ [
241
+ "OPTIONS (",
242
+ `user '${escapeSqlLiteral(runtime.staging.user)}', `,
243
+ `password '${escapeSqlLiteral(runtime.staging.password)}'`,
244
+ ")",
245
+ ].join(""),
220
246
  ].join(" "), dataDir, options, runtime.target);
221
247
  }
222
248
  function cleanupForeignStagingServer(dataDir, options, runtime) {
@@ -224,7 +250,7 @@ function cleanupForeignStagingServer(dataDir, options, runtime) {
224
250
  }
225
251
  function mountForeignStagingSchema(dataset, dataDir, options, runtime) {
226
252
  const stagingSchema = stagingSchemaName(dataset.database);
227
- const importForeignSchemaCommand = `IMPORT FOREIGN SCHEMA ${stagingSchema} FROM SERVER ${stagingServerName} INTO ${stagingSchema}`;
253
+ const importForeignSchemaCommand = `IMPORT FOREIGN SCHEMA ${stagingSchema} ` + `FROM SERVER ${stagingServerName} INTO ${stagingSchema}`;
228
254
  runPsqlCommand(`DROP SCHEMA IF EXISTS ${stagingSchema} CASCADE`, dataDir, options, runtime.target, false);
229
255
  runPsqlCommand(`CREATE SCHEMA ${stagingSchema}`, dataDir, options, runtime.target);
230
256
  try {
@@ -342,6 +368,97 @@ function listPrefixedTables(dataset, dataDir, options, runtime) {
342
368
  .map((tableName) => tableName.trim())
343
369
  .filter((tableName) => tableName.length > 0);
344
370
  }
371
+ function toPascalCase(text) {
372
+ return text.replace(/(^|_)([a-z0-9])/gi, (_match, _separator, letter) => letter.toUpperCase());
373
+ }
374
+ function toIdentifierName(tableName, columnName) {
375
+ return `${toPascalCase(tableName)}${toPascalCase(columnName)}`;
376
+ }
377
+ function trimComment(comment) {
378
+ const trimmed = comment?.trim();
379
+ return trimmed ? trimmed : undefined;
380
+ }
381
+ async function generateRawTypes(dataset, runtime, prefixedTables) {
382
+ await fs.ensureDir(rawTypesDir);
383
+ const definitionFilePath = getGeneratedDefinitionPath(dataset.database);
384
+ const manifestFilePath = getGeneratedTableManifestPath(dataset.database);
385
+ const datasetPrefix = `${dataset.database}_`;
386
+ const datasetOutputPath = path.join(rawTypesDir, dataset.database);
387
+ const generateZodSchemas = makeGenerateZodSchemas({
388
+ castToSchema: true,
389
+ getZodIdentifierMetadata: (column, details) => ({
390
+ name: `${toIdentifierName(stripDatasetPrefix(details.name, dataset.database), column.name)}Schema`,
391
+ }),
392
+ getZodSchemaMetadata: (details, generateFor) => {
393
+ const baseName = toPascalCase(stripDatasetPrefix(details.name, dataset.database));
394
+ const suffix = generateFor === "selector" || generateFor === undefined ? "" : toPascalCase(generateFor);
395
+ return {
396
+ name: `${baseName}${suffix}Schema`,
397
+ path: datasetOutputPath,
398
+ };
399
+ },
400
+ });
401
+ await fs.remove(definitionFilePath);
402
+ await fs.remove(manifestFilePath);
403
+ await processDatabase({
404
+ connection: {
405
+ database: runtime.target.name,
406
+ host: runtime.target.host,
407
+ password: runtime.target.password,
408
+ port: runtime.target.port,
409
+ user: runtime.target.user,
410
+ },
411
+ filter: (pgType) => pgType.schemaName === senatSchemaName && pgType.name.startsWith(datasetPrefix),
412
+ generators: [
413
+ makePgTsGenerator({
414
+ filter: (pgType) => pgType.schemaName === senatSchemaName && pgType.name.startsWith(datasetPrefix),
415
+ generateIdentifierType: (column, details, builtinType) => {
416
+ const tableName = stripDatasetPrefix(details.name, dataset.database);
417
+ return {
418
+ ...builtinType,
419
+ comment: undefined,
420
+ name: toIdentifierName(tableName, column.name),
421
+ typeDefinition: builtinType.typeDefinition.map((typeDefinition) => typeDefinition.replace(/ & \{ __flavor\?: '[^']+' \}/g, "").replace(/ & \{ __brand: '[^']+' \}/g, "")),
422
+ };
423
+ },
424
+ getMetadata: (details, generateFor, builtinMetadata) => {
425
+ const baseName = toPascalCase(stripDatasetPrefix(details.name, dataset.database));
426
+ const suffix = generateFor === "selector" || generateFor === undefined ? "" : toPascalCase(generateFor);
427
+ const tableComment = trimComment(details.comment);
428
+ return {
429
+ ...builtinMetadata,
430
+ comment: tableComment ? [tableComment] : undefined,
431
+ exportAs: "named",
432
+ name: `${baseName}${suffix}`,
433
+ path: datasetOutputPath,
434
+ };
435
+ },
436
+ getPropertyMetadata: (property, _details, generateFor, builtinMetadata) => {
437
+ const comment = trimComment(property.comment);
438
+ const defaultComment = generateFor === "initializer" && property.defaultValue !== null && property.defaultValue !== undefined
439
+ ? `Default value: ${property.defaultValue}`
440
+ : undefined;
441
+ const comments = [comment, defaultComment].filter((value) => value !== undefined);
442
+ return {
443
+ ...builtinMetadata,
444
+ comment: comments.length > 0 ? comments : undefined,
445
+ };
446
+ },
447
+ preRenderHooks: [generateZodSchemas],
448
+ }),
449
+ ],
450
+ outputPath: rawTypesDir,
451
+ postRenderHooks: [markAsGenerated, formatWithPrettier],
452
+ schemaNames: [senatSchemaName],
453
+ typescriptConfig: {
454
+ enumStyle: "literal-union",
455
+ tsModuleFormat: "esm",
456
+ },
457
+ });
458
+ const generatedDefinition = await fs.readFile(definitionFilePath, { encoding: "utf8" });
459
+ await fs.writeFile(definitionFilePath, generatedDefinition.replace(/\r\n/g, "\n"));
460
+ await fs.writeFile(manifestFilePath, buildGeneratedTableManifest(dataset.database, prefixedTables));
461
+ }
345
462
  async function downloadFile(url, dest) {
346
463
  const response = await fetch(url);
347
464
  if (!response.ok) {
@@ -474,7 +591,7 @@ async function retrieveDataset(dataDir, dataset, options, runtime) {
474
591
  repairedSqlWriter.end();
475
592
  await fs.move(repairedSqlFilePath, sqlFilePath, { overwrite: true });
476
593
  }
477
- if (options["all"] || options["import"] || options["schema"]) {
594
+ if (options["all"] || options["import"]) {
478
595
  if (!options["silent"]) {
479
596
  console.log(`Merging ${dataset.title}: ${sqlFilename} into ${runtime.target.name}.${senatSchemaName}...`);
480
597
  }
@@ -485,24 +602,13 @@ async function retrieveDataset(dataDir, dataset, options, runtime) {
485
602
  }
486
603
  }
487
604
  if (options["schema"]) {
488
- const definitionsDir = path.resolve("src", "raw_types_schemats");
489
- assert(fs.statSync(definitionsDir).isDirectory());
605
+ await fs.ensureDir(rawTypesDir);
490
606
  if (!options["silent"]) {
491
607
  console.log(`Creating TypeScript definitions from prefixed ${senatSchemaName} tables ` +
492
608
  `for '${dataset.database}' in database '${runtime.target.name}'...`);
493
609
  }
494
- const dbConnectionString = `postgres://${runtime.target.user}:${runtime.target.password}` +
495
- `@${runtime.target.host}:${runtime.target.port}/${runtime.target.name}`;
496
- const definitionFilePath = path.join(definitionsDir, `${dataset.database}.ts`);
497
- const tables = listPrefixedTables(dataset, dataDir, options, runtime);
498
- const tableOptions = tables.map((tableName) => `-t ${tableName}`).join(" ");
499
- execSync(`npx schemats generate -c ${dbConnectionString} -s ${senatSchemaName} ${tableOptions} -o ${definitionFilePath}`, {
500
- env: process.env,
501
- encoding: "utf-8",
502
- });
503
- const definition = fs.readFileSync(definitionFilePath, { encoding: "utf8" });
504
- const definitionRepaired = normalizeGeneratedDefinition(definition, dataset.database);
505
- fs.writeFileSync(definitionFilePath, definitionRepaired);
610
+ const prefixedTables = listPrefixedTables(dataset, dataDir, options, runtime);
611
+ await generateRawTypes(dataset, runtime, prefixedTables);
506
612
  }
507
613
  }
508
614
  function buildRuntimeContext() {
@@ -1,7 +1,10 @@
1
1
  export declare const senatSchemaName = "senat";
2
+ export declare const rawTypesDir: string;
2
3
  export declare function prefixedName(datasetName: string, relationName: string): string;
3
4
  export declare function stagingSchemaName(datasetName: string): string;
4
5
  export declare function escapeRegExp(text: string): string;
5
- export declare function normalizeGeneratedDefinition(definition: string, datasetName: string): string;
6
- export declare function extractTableNamesFromGeneratedDefinition(definition: string): string[];
7
- export declare function extractPrefixedTableNamesFromGeneratedDefinition(definition: string, datasetName: string): string[];
6
+ export declare function stripDatasetPrefix(relationName: string, datasetName: string): string;
7
+ export declare function getGeneratedDefinitionPath(datasetName: string): string;
8
+ export declare function getGeneratedTableManifestPath(datasetName: string): string;
9
+ export declare function buildGeneratedTableManifest(datasetName: string, prefixedTables: string[]): string;
10
+ export declare function extractPrefixedTableNamesFromGeneratedManifest(manifest: string, datasetName: string): string[];
@@ -1,4 +1,6 @@
1
+ import path from "path";
1
2
  export const senatSchemaName = "senat";
3
+ export const rawTypesDir = path.resolve("src", "raw_types");
2
4
  export function prefixedName(datasetName, relationName) {
3
5
  return `${datasetName}_${relationName}`;
4
6
  }
@@ -8,23 +10,27 @@ export function stagingSchemaName(datasetName) {
8
10
  export function escapeRegExp(text) {
9
11
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
12
  }
11
- export function normalizeGeneratedDefinition(definition, datasetName) {
13
+ export function stripDatasetPrefix(relationName, datasetName) {
12
14
  const datasetPrefix = `${datasetName}_`;
13
- return definition
14
- .replace(/\r\n/g, "\n")
15
- .replace(/AUTO-GENERATED FILE @ \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/, "AUTO-GENERATED FILE")
16
- .replace(new RegExp(`\\b${escapeRegExp(datasetPrefix)}([A-Za-z0-9_]+Fields)\\b`, "g"), "$1")
17
- .replace(new RegExp(`\\b${escapeRegExp(datasetPrefix)}([A-Za-z0-9_]+)\\b`, "g"), "$1");
18
- }
19
- export function extractTableNamesFromGeneratedDefinition(definition) {
20
- const flattenedDefinition = definition.replace(/\n \*/g, " ");
21
- const match = flattenedDefinition.match(/\$ schemats generate .*?((?: -t [^ ]+)+) -s [^\s]+/s);
22
- if (!match) {
15
+ return relationName.startsWith(datasetPrefix) ? relationName.slice(datasetPrefix.length) : relationName;
16
+ }
17
+ export function getGeneratedDefinitionPath(datasetName) {
18
+ return path.join(rawTypesDir, `${datasetName}.ts`);
19
+ }
20
+ export function getGeneratedTableManifestPath(datasetName) {
21
+ return path.join(rawTypesDir, `${datasetName}.tables.json`);
22
+ }
23
+ export function buildGeneratedTableManifest(datasetName, prefixedTables) {
24
+ const manifest = {
25
+ dataset: datasetName,
26
+ prefixedTables: [...new Set(prefixedTables)],
27
+ };
28
+ return JSON.stringify(manifest, null, 2) + "\n";
29
+ }
30
+ export function extractPrefixedTableNamesFromGeneratedManifest(manifest, datasetName) {
31
+ const parsed = JSON.parse(manifest);
32
+ if (parsed.dataset !== datasetName || !Array.isArray(parsed.prefixedTables)) {
23
33
  return [];
24
34
  }
25
- const tables = Array.from(match[1].matchAll(/-t ([^ ]+)/g), ([, tableName]) => tableName);
26
- return [...new Set(tables)];
27
- }
28
- export function extractPrefixedTableNamesFromGeneratedDefinition(definition, datasetName) {
29
- return extractTableNamesFromGeneratedDefinition(definition).map((tableName) => prefixedName(datasetName, tableName));
35
+ return [...new Set(parsed.prefixedTables.filter((tableName) => typeof tableName === "string"))];
30
36
  }
@@ -1,10 +1,9 @@
1
1
  import commandLineArgs from "command-line-args";
2
2
  import fs from "fs-extra";
3
- import path from "path";
4
3
  import { sql } from "../databases_postgres";
5
4
  import { getChosenDatasets, getEnabledDatasets } from "../datasets";
6
5
  import { categoriesOption, silentOption } from "./shared/cli_helpers";
7
- import { extractPrefixedTableNamesFromGeneratedDefinition, senatSchemaName } from "./shared/prefixed_tables";
6
+ import { extractPrefixedTableNamesFromGeneratedManifest, getGeneratedTableManifestPath, senatSchemaName, } from "./shared/prefixed_tables";
8
7
  const optionsDefinitions = [categoriesOption, silentOption];
9
8
  const options = commandLineArgs(optionsDefinitions);
10
9
  async function listPrefixedSenatTables(dataset) {
@@ -19,12 +18,12 @@ async function listPrefixedSenatTables(dataset) {
19
18
  return rows.map(({ relation_name }) => relation_name);
20
19
  }
21
20
  function getGeneratedDefinitionPath(dataset) {
22
- return path.resolve("src", "raw_types_schemats", `${dataset.database}.ts`);
21
+ return getGeneratedTableManifestPath(dataset.database);
23
22
  }
24
23
  function listExpectedPrefixedSenatTables(dataset) {
25
- const definitionFilePath = getGeneratedDefinitionPath(dataset);
26
- const definition = fs.readFileSync(definitionFilePath, { encoding: "utf8" });
27
- return extractPrefixedTableNamesFromGeneratedDefinition(definition, dataset.database);
24
+ const manifestFilePath = getGeneratedDefinitionPath(dataset);
25
+ const manifest = fs.readFileSync(manifestFilePath, { encoding: "utf8" });
26
+ return extractPrefixedTableNamesFromGeneratedManifest(manifest, dataset.database);
28
27
  }
29
28
  async function listColumns(schema, relationName) {
30
29
  const rows = await sql `
@@ -1,5 +1,5 @@
1
- import { ses as Ses, sub as Sub, subFields, txt_ameli } from "../raw_types_schemats/ameli";
2
- export type { Ses, Sub };
3
- export interface TxtAmeli extends txt_ameli {
4
- subids?: subFields.id[];
1
+ import { TxtAmeli } from "../raw_types/ameli";
2
+ import { Sub } from "../raw_types/ameli";
3
+ export interface TxtAmeliCustom extends TxtAmeli {
4
+ subids?: Sub["id"][];
5
5
  }
@@ -1,2 +1,2 @@
1
- import { debats as Debat, lecassdeb as LecAssDeb } from "../raw_types_schemats/debats";
2
- export type { Debat, LecAssDeb };
1
+ import { Debats, Lecassdeb } from "../raw_types/debats";
2
+ export type { Debats as Debat, Lecassdeb as LecAssDeb };
@@ -1,70 +1,70 @@
1
- import { txt_ameliFields } from "../raw_types_schemats/ameli";
2
- import { ass as Ass, aud, audFields, auteur, date_seance, date_seanceFields as dateSeanceFields, deccoc as DecCoc, denrap as DenRap, docatt, docattFields, ecr, ecrFields, etaloi as EtaLoi, lecass, lecassFields, lecassrap, lecture, lectureFields, loi, org as Org, oritxt as OriTxt, qua as Qua, rap, raporg as RapOrg, raporgFields, scr as Scr, texte, texteFields, typatt as TypAtt, typlec as TypLec, typloi as TypLoi, typtxt as TypTxt, typurl as TypUrl, typurlFields } from "../raw_types_schemats/dosleg";
3
- import { Debat } from "./debats";
4
- import { TxtAmeli } from "./ameli";
5
- export type { Ass, DecCoc, DenRap, EtaLoi, Org, OriTxt, Qua, RapOrg, Scr, TypAtt, TypLec, TypLoi, TypTxt, TypUrl };
6
- export interface Aud extends aud {
1
+ import { Ass, Aud as AudRaw, Auteur as AuteurRaw, DateSeance as DateSeanceRaw, Deccoc, Denrap, Docatt as DocattRaw, Ecr as EcrRaw, Etaloi, Lecass as LecassRaw, Lecassrap as LecassrapRaw, Lecture as LectureRaw, Loi as LoiRaw, Org, Oritxt, Qua, Rap as RapRaw, Raporg, Scr, Texte as TexteRaw, Typatt, Typlec, Typloi, Typtxt, Typurl } from "../raw_types/dosleg";
2
+ import { TxtAmeli } from "../raw_types/ameli";
3
+ import { Debats } from "../raw_types/debats";
4
+ import { TxtAmeliCustom } from "./ameli";
5
+ export type { Deccoc as DecCoc, Denrap as DenRap, Etaloi as EtaLoi, Oritxt as OriTxt, Raporg as RapOrg, Typatt as TypAtt, Typlec as TypLec, Typloi as TypLoi, Typtxt as TypTxt, Typurl as TypUrl, };
6
+ export interface Aud extends AudRaw {
7
7
  org?: Org;
8
8
  }
9
- export interface Auteur extends auteur {
9
+ export interface Auteur extends AuteurRaw {
10
10
  qua?: Qua;
11
11
  }
12
- export interface DateSeance extends date_seance {
13
- debat?: Debat;
12
+ export interface DateSeance extends DateSeanceRaw {
13
+ debat?: Debats;
14
14
  scrids?: string[];
15
15
  scrs?: Scr[];
16
16
  }
17
- export interface DocAtt extends docatt {
18
- rap?: Rap;
19
- typatt?: TypAtt;
17
+ export interface DocAtt extends DocattRaw {
18
+ rap?: RapRaw;
19
+ typatt?: Typatt;
20
20
  }
21
- export interface Ecr extends ecr {
21
+ export interface Ecr extends EcrRaw {
22
22
  aut?: Auteur;
23
23
  }
24
- export interface LecAss extends lecass {
24
+ export interface LecAss extends LecassRaw {
25
25
  ass?: Ass;
26
26
  auds?: Aud[];
27
- audcles?: audFields.audcle[];
27
+ audcles?: AudRaw["audcle"][];
28
28
  datesSeances?: DateSeance[];
29
- datesSeancesCodes?: dateSeanceFields.code[];
29
+ datesSeancesCodes?: DateSeanceRaw["code"][];
30
30
  debatdatseas: Date[];
31
31
  lecassraps?: LecAssRap[];
32
32
  lecassrapids?: string[];
33
33
  org?: Org;
34
- texcods: texteFields.texcod[];
34
+ texcods: TexteRaw["texcod"][];
35
35
  textes?: Texte[];
36
36
  }
37
- export interface LecAssRap extends lecassrap {
38
- rap?: Rap;
37
+ export interface LecAssRap extends LecassrapRaw {
38
+ rap?: RapRaw;
39
39
  }
40
- export interface Lecture extends lecture {
41
- typlec?: TypLec;
42
- lecassidts?: lecassFields.lecassidt[];
40
+ export interface Lecture extends LectureRaw {
41
+ typlec?: Typlec;
42
+ lecassidts?: LecassRaw["lecassidt"][];
43
43
  lecasss?: LecAss[];
44
44
  }
45
- export interface Loi extends loi {
46
- deccoc?: DecCoc;
47
- etaloi?: EtaLoi;
48
- lecidts?: lectureFields.lecidt[];
45
+ export interface Loi extends LoiRaw {
46
+ deccoc?: Deccoc;
47
+ etaloi?: Etaloi;
48
+ lecidts?: LectureRaw["lecidt"][];
49
49
  lectures?: Lecture[];
50
- typloi?: TypLoi;
50
+ typloi?: Typloi;
51
51
  }
52
- export interface Rap extends rap {
53
- denrap?: DenRap;
54
- docattcles?: docattFields.docattcle[];
52
+ export interface Rap extends RapRaw {
53
+ denrap?: Denrap;
54
+ docattcles?: DocattRaw["docattcle"][];
55
55
  docatts?: DocAtt[];
56
- ecrnums?: ecrFields.ecrnum[];
56
+ ecrnums?: EcrRaw["ecrnum"][];
57
57
  ecrs?: Ecr[];
58
- orgcods?: raporgFields.orgcod[];
58
+ orgcods?: Raporg["orgcod"][];
59
59
  orgs?: Org[];
60
60
  }
61
- export interface Texte extends texte {
61
+ export interface Texte extends TexteRaw {
62
62
  ecrs?: Ecr[];
63
- ecrnums?: ecrFields.ecrnum[];
64
- libtypurl?: typurlFields.libtypurl;
63
+ ecrnums?: EcrRaw["ecrnum"][];
64
+ libtypurl?: Typurl["libtypurl"];
65
65
  org?: Org;
66
- oritxt: OriTxt;
67
- typtxt?: TypTxt;
68
- txtAmeli?: TxtAmeli;
69
- txtAmeliId: txt_ameliFields.id;
66
+ oritxt: Oritxt;
67
+ typtxt?: Typtxt;
68
+ txtAmeli?: TxtAmeliCustom;
69
+ txtAmeliId: TxtAmeli["id"];
70
70
  }
@@ -1,2 +1,2 @@
1
- import { tam_questions as Question } from "../raw_types_schemats/questions";
2
- export type { Question };
1
+ import { TamQuestions } from "../raw_types/questions";
2
+ export type { TamQuestions as Question };
@@ -1,5 +1,3 @@
1
- import { sen as Sen } from "../raw_types_schemats/sens";
2
- export type { Sen };
3
1
  export interface Photo {
4
2
  chemin: string;
5
3
  cheminMosaique: string;
@@ -1,26 +1,19 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { extractPrefixedTableNamesFromGeneratedDefinition, normalizeGeneratedDefinition, prefixedName, senatSchemaName, stagingSchemaName, } from "../src/scripts/shared/prefixed_tables";
2
+ import { buildGeneratedTableManifest, extractPrefixedTableNamesFromGeneratedManifest, prefixedName, senatSchemaName, stagingSchemaName, stripDatasetPrefix, } from "../src/scripts/shared/prefixed_tables";
3
3
  describe("prefixed table helpers", () => {
4
4
  it("builds prefixed table and staging schema names", () => {
5
5
  expect(prefixedName("dosleg", "texte")).toBe("dosleg_texte");
6
6
  expect(stagingSchemaName("questions")).toBe("questions_staging");
7
7
  expect(senatSchemaName).toBe("senat");
8
8
  });
9
- it("normalizes generated definitions by removing dataset prefixes", () => {
10
- const definition = "/* tslint:disable */\n/**\n * AUTO-GENERATED FILE @ 2026-03-20 16:00:00\n" +
11
- " * $ schemats generate -c postgres://demo -t ameli_amd -t ameli_ses -s senat\n" +
12
- " */\nexport namespace ameli_amdFields {}\nexport interface ameli_amd {}\nexport interface ameli_ses {}\n";
13
- const normalized = normalizeGeneratedDefinition(definition, "ameli");
14
- expect(normalized).toContain("AUTO-GENERATED FILE");
15
- expect(normalized).not.toContain("AUTO-GENERATED FILE @");
16
- expect(normalized).toContain("export namespace amdFields");
17
- expect(normalized).toContain("export interface amd");
18
- expect(normalized).toContain("export interface ses");
19
- expect(normalized).not.toContain("ameli_amd");
9
+ it("strips the dataset prefix from generated type names", () => {
10
+ expect(stripDatasetPrefix("ameli_amd", "ameli")).toBe("amd");
11
+ expect(stripDatasetPrefix("ameli_txt_ameli", "ameli")).toBe("txt_ameli");
12
+ expect(stripDatasetPrefix("amd", "ameli")).toBe("amd");
20
13
  });
21
- it("extracts expected prefixed public tables from generated definitions", () => {
22
- const definition = "/**\n * $ schemats generate -c postgres://demo -t amd -t ses -t txt_ameli -s ameli\n */";
23
- expect(extractPrefixedTableNamesFromGeneratedDefinition(definition, "ameli")).toEqual([
14
+ it("extracts expected prefixed public tables from generated manifests", () => {
15
+ const manifest = buildGeneratedTableManifest("ameli", ["ameli_amd", "ameli_ses", "ameli_txt_ameli"]);
16
+ expect(extractPrefixedTableNamesFromGeneratedManifest(manifest, "ameli")).toEqual([
24
17
  "ameli_amd",
25
18
  "ameli_ses",
26
19
  "ameli_txt_ameli",
@@ -1,12 +1,12 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { datasets } from "../src/datasets";
4
- import { extractPrefixedTableNamesFromGeneratedDefinition } from "../src/scripts/shared/prefixed_tables";
4
+ import { extractPrefixedTableNamesFromGeneratedManifest, getGeneratedTableManifestPath, } from "../src/scripts/shared/prefixed_tables";
5
5
  describe("generated raw types coverage", () => {
6
6
  it("maps each generated dataset definition to prefixed senat tables", () => {
7
7
  for (const dataset of Object.values(datasets)) {
8
- const definition = readFileSync(`src/raw_types_schemats/${dataset.database}.ts`, { encoding: "utf8" });
9
- const tableNames = extractPrefixedTableNamesFromGeneratedDefinition(definition, dataset.database);
8
+ const manifest = readFileSync(getGeneratedTableManifestPath(dataset.database), { encoding: "utf8" });
9
+ const tableNames = extractPrefixedTableNamesFromGeneratedManifest(manifest, dataset.database);
10
10
  expect(tableNames.length).toBeGreaterThan(0);
11
11
  expect(tableNames.every((tableName) => tableName.startsWith(`${dataset.database}_`))).toBe(true);
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/senat",
3
- "version": "2.23.0",
3
+ "version": "3.0.0",
4
4
  "description": "Handle French Sénat's open data",
5
5
  "keywords": [
6
6
  "France",
@@ -46,6 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "scripts": {
49
+ "postinstall": "patch-package",
49
50
  "build": "tsc",
50
51
  "build:types": "tsc --emitDeclarationOnly",
51
52
  "data:convert_data": "tsx src/scripts/convert_data.ts",
@@ -86,7 +87,6 @@
86
87
  "zod": "^4.3.5"
87
88
  },
88
89
  "devDependencies": {
89
- "@typed-code/schemats": "^5.0.1",
90
90
  "@types/cheerio": "^1.0.0",
91
91
  "@types/command-line-args": "^5.0.0",
92
92
  "@types/fs-extra": "^11.0.4",
@@ -99,6 +99,10 @@
99
99
  "cross-env": "^10.1.0",
100
100
  "eslint": "^9.39.2",
101
101
  "globals": "^17.0.0",
102
+ "kanel": "^4.0.1",
103
+ "kanel-zod": "^4.0.0",
104
+ "patch-package": "^8.0.1",
105
+ "postinstall-postinstall": "^2.1.0",
102
106
  "prettier": "^3.5.3",
103
107
  "tsx": "^4.21.0",
104
108
  "typescript": "^5.9.3",