@nodefony/drizzle 10.0.0-alpha.8 → 10.0.0-beta.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/README.md +19 -19
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +9 -6
- package/dist/nodefony/command/migrateShared.js +17 -1
- package/dist/nodefony/command/orm-generate.js +14 -10
- package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
- package/dist/nodefony/command/orm-reset.js +1 -0
- package/dist/nodefony/config/defineModuleConfig.js +23 -7
- package/dist/nodefony/entity/userTable.js +4 -1
- package/dist/nodefony/registerStores.js +4 -28
- package/dist/nodefony/service/DrizzleService.js +42 -16
- package/dist/nodefony/src/DrizzleAuditStore.js +14 -2
- package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
- package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
- package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
- package/dist/nodefony/src/SessionStorage.js +8 -0
- package/dist/nodefony/src/connectorTarget.js +1 -1
- package/dist/nodefony/src/frameworkConnector.js +29 -0
- package/dist/nodefony/src/memoryDatabase.js +13 -0
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
- package/dist/nodefony/src/migrator/adopt.js +1 -1
- package/dist/nodefony/src/migrator/appSchema.js +44 -1
- package/dist/nodefony/src/migrator/destructive.js +2 -3
- package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +1 -1
- package/dist/nodefony/src/migrator/drivers/postgresDriver.js +1 -1
- package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
- package/dist/nodefony/src/migrator/explain.js +7 -10
- package/dist/nodefony/src/migrator/history.js +25 -2
- package/dist/nodefony/src/migrator/kit.js +41 -6
- package/dist/nodefony/src/migrator/paths.js +55 -1
- package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
- package/dist/nodefony/src/migrator/refusals.js +50 -1
- package/dist/nodefony/src/migrator/resolve.js +59 -9
- package/dist/nodefony/src/migrator/sources.js +13 -8
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +51 -30
- package/dist/nodefony/src/orm-core/DrizzleRepository.js +38 -26
- package/dist/nodefony/src/queryKit.js +5 -5
- package/dist/nodefony/src/safeTarget.js +1 -0
- package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
- package/dist/types/nodefony/entity/userTable.d.ts +46 -3
- package/dist/types/nodefony/registerStores.d.ts +4 -3
- package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
- package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
- package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
- package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
- package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
- package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
- package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
- package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/drivers/index.d.ts +2 -2
- package/dist/types/nodefony/src/migrator/explain.d.ts +9 -9
- package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
- package/dist/types/nodefony/src/migrator/name.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
- package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
- package/dist/types/nodefony/src/migrator/resolve.d.ts +44 -1
- package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +13 -2
- package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +1 -1
- package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +2 -2
- package/dist/types/nodefony/src/queryKit.d.ts +9 -9
- package/dist/types/nodefony/src/safeTarget.d.ts +2 -2
- package/docs/index.md +31 -28
- package/docs/migrations.md +39 -3
- package/package.json +19 -20
|
@@ -104,8 +104,10 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
|
|
|
104
104
|
* base ({@link DrizzleOrm.location}) pour Studio. Lazy (comme `resolveDb`) car
|
|
105
105
|
* le store est fabriqué AVANT le connect de l'ORM → l'emplacement n'est lisible
|
|
106
106
|
* qu'une fois l'ORM enregistré (lu au `onReady`, pas à la construction).
|
|
107
|
+
* @param connector - connecteur ORM qui porte la table, publié au registre des stores.
|
|
107
108
|
*/
|
|
108
|
-
constructor(resolveDb, now = Date.now, leaseMs = DEFAULT_LEASE_MS, ttlMs = DEFAULT_TTL_MS, table = idempotencyKeyTable, resolveLocation) {
|
|
109
|
+
constructor(resolveDb, now = Date.now, leaseMs = DEFAULT_LEASE_MS, ttlMs = DEFAULT_TTL_MS, table = idempotencyKeyTable, resolveLocation, connector) {
|
|
110
|
+
this.#connector = connector;
|
|
109
111
|
this.#resolveDb = resolveDb;
|
|
110
112
|
this.#table = table;
|
|
111
113
|
this.#c = execTable(table);
|
|
@@ -124,6 +126,16 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
|
|
|
124
126
|
get location() {
|
|
125
127
|
return this.#resolveLocation?.();
|
|
126
128
|
}
|
|
129
|
+
/** Connecteur ORM qui porte ce store — reçu du constructeur. */
|
|
130
|
+
#connector;
|
|
131
|
+
/**
|
|
132
|
+
* Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
|
|
133
|
+
* registre des stores : la console rattache la brique à SON connecteur au
|
|
134
|
+
* lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
|
|
135
|
+
*/
|
|
136
|
+
get connector() {
|
|
137
|
+
return this.#connector;
|
|
138
|
+
}
|
|
127
139
|
/**
|
|
128
140
|
* Construit le store depuis un {@link DrizzleOrm}. Le handle est résolu **lazy**
|
|
129
141
|
* (gardé par `isConnected()` → `null` tant que l'ORM n'est pas/plus connecté).
|
|
@@ -137,7 +149,7 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
|
|
|
137
149
|
* @param ttlMs - rétention d'une réponse mémorisée (ms).
|
|
138
150
|
*/
|
|
139
151
|
static from(orm, now, leaseMs, ttlMs) {
|
|
140
|
-
return new DrizzleIdempotencyStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, leaseMs, ttlMs, createIdempotencyTable(orm.dialect), () => orm.location);
|
|
152
|
+
return new DrizzleIdempotencyStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, leaseMs, ttlMs, createIdempotencyTable(orm.dialect), () => orm.location, orm.name);
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
155
|
* Approximation **per-pod, best-effort** : compteur local des réservations
|
|
@@ -75,6 +75,16 @@ var DrizzleTokenStore = class DrizzleTokenStore {
|
|
|
75
75
|
get location() {
|
|
76
76
|
return this.#location;
|
|
77
77
|
}
|
|
78
|
+
/** Connecteur ORM qui porte ce store — posé par {@link DrizzleTokenStore.from}. */
|
|
79
|
+
#connector;
|
|
80
|
+
/**
|
|
81
|
+
* Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
|
|
82
|
+
* registre des stores : la console rattache la brique à SON connecteur au
|
|
83
|
+
* lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
|
|
84
|
+
*/
|
|
85
|
+
get connector() {
|
|
86
|
+
return this.#connector;
|
|
87
|
+
}
|
|
78
88
|
/**
|
|
79
89
|
* Construit le store depuis un {@link DrizzleOrm} connecté. Les entités
|
|
80
90
|
* (`registerTokenEntities`) doivent avoir été enregistrées **avant**
|
|
@@ -85,7 +95,9 @@ var DrizzleTokenStore = class DrizzleTokenStore {
|
|
|
85
95
|
* @param retentionRevokedMs - rétention des PAT révoqués sans `exp`.
|
|
86
96
|
*/
|
|
87
97
|
static from(orm, now, retentionRevokedMs) {
|
|
88
|
-
|
|
98
|
+
const store = new DrizzleTokenStore(orm.getRepository(TOKEN_ENTITY_NAMES.records), orm.getRepository(TOKEN_ENTITY_NAMES.denied), orm.getRepository(TOKEN_ENTITY_NAMES.revocations), now, retentionRevokedMs, orm.location);
|
|
99
|
+
store.#connector = orm.name;
|
|
100
|
+
return store;
|
|
89
101
|
}
|
|
90
102
|
/**
|
|
91
103
|
* Insère ou remplace un record (PAT / refresh) — 1 requête, `upsert` atomique
|
|
@@ -40,6 +40,16 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
|
|
|
40
40
|
get location() {
|
|
41
41
|
return this.#location;
|
|
42
42
|
}
|
|
43
|
+
/** Connecteur ORM qui porte ce store — posé par {@link DrizzleTotpSecretStore.from}. */
|
|
44
|
+
#connector;
|
|
45
|
+
/**
|
|
46
|
+
* Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
|
|
47
|
+
* registre des stores : la console rattache la brique à SON connecteur au
|
|
48
|
+
* lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
|
|
49
|
+
*/
|
|
50
|
+
get connector() {
|
|
51
|
+
return this.#connector;
|
|
52
|
+
}
|
|
43
53
|
/**
|
|
44
54
|
* Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
|
|
45
55
|
* (`registerTotpSecretEntity`) doit avoir été enregistrée **avant** `orm.connect()`.
|
|
@@ -47,7 +57,9 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
|
|
|
47
57
|
* @param orm - ORM Drizzle connecté hébergeant la table du store.
|
|
48
58
|
*/
|
|
49
59
|
static from(orm) {
|
|
50
|
-
|
|
60
|
+
const store = new DrizzleTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY), orm.location);
|
|
61
|
+
store.#connector = orm.name;
|
|
62
|
+
return store;
|
|
51
63
|
}
|
|
52
64
|
/** Row plate → secret du contrat (recoveryCodes copié = mutable indépendant). */
|
|
53
65
|
#toSecret(row) {
|
|
@@ -39,6 +39,16 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
|
|
|
39
39
|
get location() {
|
|
40
40
|
return this.#location;
|
|
41
41
|
}
|
|
42
|
+
/** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebAuthnCredentialStore.from}. */
|
|
43
|
+
#connector;
|
|
44
|
+
/**
|
|
45
|
+
* Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
|
|
46
|
+
* registre des stores : la console rattache la brique à SON connecteur au
|
|
47
|
+
* lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
|
|
48
|
+
*/
|
|
49
|
+
get connector() {
|
|
50
|
+
return this.#connector;
|
|
51
|
+
}
|
|
42
52
|
/**
|
|
43
53
|
* Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
|
|
44
54
|
* (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
|
|
@@ -47,7 +57,9 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
|
|
|
47
57
|
* @param orm - ORM Drizzle connecté hébergeant la table du store.
|
|
48
58
|
*/
|
|
49
59
|
static from(orm) {
|
|
50
|
-
|
|
60
|
+
const store = new DrizzleWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY), orm.location);
|
|
61
|
+
store.#connector = orm.name;
|
|
62
|
+
return store;
|
|
51
63
|
}
|
|
52
64
|
/** Row plate → credential du contrat (`nickname?` omis si `null`). */
|
|
53
65
|
#toCredential(row) {
|
|
@@ -59,6 +59,16 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
|
|
|
59
59
|
get location() {
|
|
60
60
|
return this.#location;
|
|
61
61
|
}
|
|
62
|
+
/** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebhookStore.from}. */
|
|
63
|
+
#connector;
|
|
64
|
+
/**
|
|
65
|
+
* Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
|
|
66
|
+
* registre des stores : la console rattache la brique à SON connecteur au
|
|
67
|
+
* lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
|
|
68
|
+
*/
|
|
69
|
+
get connector() {
|
|
70
|
+
return this.#connector;
|
|
71
|
+
}
|
|
62
72
|
/**
|
|
63
73
|
* Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
|
|
64
74
|
* (`registerWebhookEndpointEntity`) doit avoir été enregistrée **avant**
|
|
@@ -67,7 +77,9 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
|
|
|
67
77
|
* @param orm - ORM Drizzle connecté hébergeant la table du store.
|
|
68
78
|
*/
|
|
69
79
|
static from(orm) {
|
|
70
|
-
|
|
80
|
+
const store = new DrizzleWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), orm.location, orm.getNativeConnection(), orm.dialect);
|
|
81
|
+
store.#connector = orm.name;
|
|
82
|
+
return store;
|
|
71
83
|
}
|
|
72
84
|
/** Row plate → endpoint du contrat (`events` mutable accepté en `readonly`). */
|
|
73
85
|
#toEndpoint(row) {
|
|
@@ -41,6 +41,14 @@ var SessionStorage = class SessionStorage {
|
|
|
41
41
|
return typeof loc === "string" && loc.length > 0 ? loc : void 0;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
+
* Connecteur ORM qui porte les sessions — lu par `readStoreConnector` pour le
|
|
45
|
+
* registre des stores. `undefined` tant que l'ORM de ce connecteur n'est pas
|
|
46
|
+
* enregistré : on ne publie pas un rattachement que rien ne porte.
|
|
47
|
+
*/
|
|
48
|
+
get connector() {
|
|
49
|
+
return ormRegistry.has("default") ? SESSION_CONNECTOR : void 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
44
52
|
* Repository de l'entité session, ou `null` si l'ORM n'est pas (ou plus)
|
|
45
53
|
* connecté.
|
|
46
54
|
*
|
|
@@ -45,7 +45,7 @@ function defaultConnectorFilename(kernel, name) {
|
|
|
45
45
|
* @returns les coordonnées, avec le fichier SQLite TOUJOURS résolu.
|
|
46
46
|
*/
|
|
47
47
|
function resolveConnectorTarget(kernel, name, cfg) {
|
|
48
|
-
const dialect = cfg.dialect
|
|
48
|
+
const dialect = cfg.dialect;
|
|
49
49
|
if (dialect !== "sqlite") return {
|
|
50
50
|
dialect,
|
|
51
51
|
url: cfg.url
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region nodefony/src/frameworkConnector.ts
|
|
2
|
+
/**
|
|
3
|
+
* Connecteur conventionnel qui héberge le schéma framework (même convention que
|
|
4
|
+
* `SESSION_CONNECTOR` : `"default"` pour Drizzle, `"nodefony"` pour Mongoose).
|
|
5
|
+
*
|
|
6
|
+
* Vit dans ce fichier sans dépendance parce que DEUX familles le lisent : le
|
|
7
|
+
* registre des stores (où résoudre les briques du framework) et les migrations
|
|
8
|
+
* (qui les reçoit). Une copie du littéral de chaque côté dériverait en silence.
|
|
9
|
+
*/
|
|
10
|
+
const FRAMEWORK_CONNECTOR = "default";
|
|
11
|
+
/**
|
|
12
|
+
* Ce connecteur possède-t-il les migrations du framework et de l'application ?
|
|
13
|
+
*
|
|
14
|
+
* **Seul le connecteur du framework les possède.** Le dossier des migrations de
|
|
15
|
+
* l'application (`migrations/<dialect>`) ne porte aucune notion de connecteur :
|
|
16
|
+
* il décrit UNE base, celle où vivent les briques du framework et les entités
|
|
17
|
+
* de l'application. Les attribuer à tout connecteur faisait qu'une base
|
|
18
|
+
* secondaire (analyse, base d'un module) en mode `migrate` y créait des tables
|
|
19
|
+
* qui ne lui appartiennent pas, et qu'en mode `auto` elle annonçait à chaque
|
|
20
|
+
* démarrage des migrations qui ne la concernent pas.
|
|
21
|
+
*
|
|
22
|
+
* @param connector - nom du connecteur (clé de `connectors`).
|
|
23
|
+
* @returns `true` pour le connecteur du framework, `false` pour tout autre.
|
|
24
|
+
*/
|
|
25
|
+
function ownsSharedMigrations(connector) {
|
|
26
|
+
return connector === FRAMEWORK_CONNECTOR;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { FRAMEWORK_CONNECTOR, ownsSharedMigrations };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region nodefony/src/memoryDatabase.ts
|
|
2
|
+
/**
|
|
3
|
+
* Nom de fichier d'une base SQLite en MÉMOIRE — volatile, elle repart vide à
|
|
4
|
+
* chaque démarrage.
|
|
5
|
+
*
|
|
6
|
+
* Module FEUILLE (aucun import) : l'adapter, le migrateur, les commandes et le
|
|
7
|
+
* service le consultent tous, et le placer dans l'un d'eux ferait importer le
|
|
8
|
+
* migrateur par l'adapter. Une seule écriture du littéral : c'est d'elle que
|
|
9
|
+
* dépend la règle « une base volatile dérive son schéma » (`resolveDdlMode`).
|
|
10
|
+
*/
|
|
11
|
+
const MEMORY_DATABASE = ":memory:";
|
|
12
|
+
//#endregion
|
|
13
|
+
export { MEMORY_DATABASE };
|
|
@@ -376,7 +376,7 @@ var DrizzleMigrator = class {
|
|
|
376
376
|
* @throws MigrationVerdictError portant le verdict structuré.
|
|
377
377
|
*/
|
|
378
378
|
#assertApplicable(plan, files, options) {
|
|
379
|
-
const first = plan.failed
|
|
379
|
+
const first = plan.failed.at(0);
|
|
380
380
|
if (first) throw this.#verdict("NF_MIGRATE_FAILED_MARKER", {
|
|
381
381
|
source: first.source,
|
|
382
382
|
tag: first.tag,
|
|
@@ -386,7 +386,7 @@ var DrizzleMigrator = class {
|
|
|
386
386
|
},
|
|
387
387
|
actions: this.#recoveryActions()
|
|
388
388
|
}, `La migration « ${first.tag} » (source « ${first.source} ») a échoué ou n'a jamais fini. Inspecter la base, puis réparer avant de reprendre — une reprise aveugle n'est jamais sûre.\n\n${REPRISE_SANS_DESTRUCTION}`);
|
|
389
|
-
const drift = plan.drifted
|
|
389
|
+
const drift = plan.drifted.at(0);
|
|
390
390
|
if (drift) throw this.#verdict("NF_MIGRATE_HASH_MISMATCH", {
|
|
391
391
|
source: drift.source,
|
|
392
392
|
tag: drift.tag,
|
|
@@ -411,7 +411,7 @@ var DrizzleMigrator = class {
|
|
|
411
411
|
]
|
|
412
412
|
}]
|
|
413
413
|
}, `Le fichier de la migration « ${drift.tag} » (source « ${drift.source} ») a changé depuis son application. Une migration déjà jouée est immuable : écrire une NOUVELLE migration, ou assumer le ré-alignement.`);
|
|
414
|
-
const gone = plan.missing
|
|
414
|
+
const gone = plan.missing.at(0);
|
|
415
415
|
if (gone && options.ignoreMissing !== true) throw this.#verdict("NF_MIGRATE_MISSING_FILE", {
|
|
416
416
|
source: gone.source,
|
|
417
417
|
tag: gone.tag,
|
|
@@ -113,7 +113,7 @@ async function snapshotTables(outDir) {
|
|
|
113
113
|
try {
|
|
114
114
|
const raw = await fs.readFile(path.join(outDir, "meta", "0000_snapshot.json"), "utf8");
|
|
115
115
|
const snapshot = JSON.parse(raw);
|
|
116
|
-
return Object.
|
|
116
|
+
return Object.entries(snapshot.tables ?? {}).map(([key, t]) => t.name ?? key);
|
|
117
117
|
} catch {
|
|
118
118
|
return [];
|
|
119
119
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FORMAT_MARKER } from "./types.js";
|
|
2
|
+
import { ownsSharedMigrations } from "../frameworkConnector.js";
|
|
2
3
|
import "./kit.js";
|
|
3
4
|
import { registerHooks } from "node:module";
|
|
4
5
|
import { entityRegistry } from "@nodefony/orm-core";
|
|
@@ -410,5 +411,47 @@ function registeredTables(connector) {
|
|
|
410
411
|
}
|
|
411
412
|
return out;
|
|
412
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Tables d'un connecteur parmi celles que les fichiers fournissent — le
|
|
416
|
+
* partage qui fait qu'une migration ne décrit que SA base.
|
|
417
|
+
*
|
|
418
|
+
* Une table appartient au connecteur de l'entité qui la déclare (le REGISTRE
|
|
419
|
+
* le sait, un fichier non). Une table qu'aucune entité ne réclame reste au
|
|
420
|
+
* connecteur du framework, comme avant que les connecteurs secondaires aient
|
|
421
|
+
* leurs migrations. Sans ce partage, la table d'une entité secondaire entrait
|
|
422
|
+
* AUSSI dans les migrations du framework : créée dans une base qui ne s'en sert
|
|
423
|
+
* jamais, et proposée à la suppression le jour où elle n'y figure plus.
|
|
424
|
+
*
|
|
425
|
+
* @param tables - tables fournies par les fichiers de l'application.
|
|
426
|
+
* @param connector - connecteur visé.
|
|
427
|
+
* @param owners - `table → connecteurs` qui la déclarent ; par défaut, le registre.
|
|
428
|
+
* @returns les tables de ce connecteur, dans l'ordre reçu.
|
|
429
|
+
*/
|
|
430
|
+
function tablesOfConnector(tables, connector, owners = registeredOwners()) {
|
|
431
|
+
return tables.filter((t) => {
|
|
432
|
+
const claimed = owners.get(t.tableName);
|
|
433
|
+
return claimed === void 0 ? ownsSharedMigrations(connector) : claimed.has(connector);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Pour chaque table enregistrée, les connecteurs dont une entité la déclare.
|
|
438
|
+
*
|
|
439
|
+
* @returns la table de correspondance, lue dans le registre des entités.
|
|
440
|
+
*/
|
|
441
|
+
function registeredOwners() {
|
|
442
|
+
const owners = /* @__PURE__ */ new Map();
|
|
443
|
+
for (const entity of entityRegistry.list()) {
|
|
444
|
+
const schema = entity.schema;
|
|
445
|
+
if (!is(schema, Table)) continue;
|
|
446
|
+
const table = getTableName(schema);
|
|
447
|
+
let set = owners.get(table);
|
|
448
|
+
if (set === void 0) {
|
|
449
|
+
set = /* @__PURE__ */ new Set();
|
|
450
|
+
owners.set(table, set);
|
|
451
|
+
}
|
|
452
|
+
set.add(entity.connector);
|
|
453
|
+
}
|
|
454
|
+
return owners;
|
|
455
|
+
}
|
|
413
456
|
//#endregion
|
|
414
|
-
export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
|
|
457
|
+
export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredOwners, registeredTables, tablesOfConnector, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
|
|
@@ -103,8 +103,7 @@ function rebuiltTables(sql) {
|
|
|
103
103
|
const out = /* @__PURE__ */ new Set();
|
|
104
104
|
const renames = joint.matchAll(new RegExp(`\\bALTER\\s+TABLE\\s+${IDENT}\\s+RENAME\\s+TO\\s+${IDENT}`, "gi"));
|
|
105
105
|
for (const rename of renames) {
|
|
106
|
-
const staging = rename
|
|
107
|
-
const target = rename[2];
|
|
106
|
+
const [, staging, target] = rename;
|
|
108
107
|
if (staging === void 0 || target === void 0) continue;
|
|
109
108
|
const created = new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"']?${staging}[\`"']?\\b`, "i");
|
|
110
109
|
const copied = new RegExp(`\\bINSERT\\s+INTO\\s+[\`"']?${staging}[\`"']?[\\s\\S]{0,4000}?\\bFROM\\s+[\`"']?${target}[\`"']?\\b`, "i");
|
|
@@ -124,7 +123,7 @@ function rebuiltTables(sql) {
|
|
|
124
123
|
* @returns les noms supprimés, en minuscules.
|
|
125
124
|
*/
|
|
126
125
|
function droppedTables(sql) {
|
|
127
|
-
return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].
|
|
126
|
+
return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
|
|
128
127
|
}
|
|
129
128
|
/**
|
|
130
129
|
* L'instruction fait-elle partie de la mécanique d'une reconstruction ?
|
|
@@ -68,7 +68,7 @@ var MysqlMigrationDriver = class {
|
|
|
68
68
|
}
|
|
69
69
|
const cx = await createConnection(this.#url);
|
|
70
70
|
cx.on("error", (error) => {
|
|
71
|
-
this.#lost = error
|
|
71
|
+
this.#lost = error instanceof Error ? error.message : String(error);
|
|
72
72
|
});
|
|
73
73
|
this.#lost = null;
|
|
74
74
|
this.#cxOrNull = cx;
|
|
@@ -56,7 +56,7 @@ var PostgresMigrationDriver = class {
|
|
|
56
56
|
}
|
|
57
57
|
const client = new ClientCtor({ connectionString: this.#url });
|
|
58
58
|
client.on("error", (error) => {
|
|
59
|
-
this.#lost = error
|
|
59
|
+
this.#lost = error instanceof Error ? error.message : String(error);
|
|
60
60
|
});
|
|
61
61
|
await client.connect();
|
|
62
62
|
this.#lost = null;
|
|
@@ -309,18 +309,15 @@ function summaryOf(plan, verdict, divergence = null) {
|
|
|
309
309
|
}
|
|
310
310
|
case "drift": {
|
|
311
311
|
const parts = [];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
if (plan.missing.length > 0) {
|
|
317
|
-
const m = plan.missing[0];
|
|
318
|
-
parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
|
|
319
|
-
}
|
|
312
|
+
const [d] = plan.drifted;
|
|
313
|
+
if (d !== void 0) parts.push(`le fichier « ${d.source}/${d.tag} » a été modifié APRÈS avoir été appliqué (son empreinte a changé)`);
|
|
314
|
+
const [m] = plan.missing;
|
|
315
|
+
if (m !== void 0) parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
|
|
320
316
|
return `Le connecteur « ${c} » ne concorde plus avec son historique : ${parts.join(" ; ")}.`;
|
|
321
317
|
}
|
|
322
318
|
case "failed": {
|
|
323
|
-
const f = plan.failed
|
|
319
|
+
const [f] = plan.failed;
|
|
320
|
+
if (f === void 0) return `Le connecteur « ${c} » porte une migration qui n'a pas abouti.`;
|
|
324
321
|
const when = f.startedAt ? new Date(f.startedAt).toISOString() : "à une date inconnue";
|
|
325
322
|
const why = f.error ? ` (erreur : ${f.error})` : "";
|
|
326
323
|
return `Le connecteur « ${c} » porte ${plan.failed.length} migration${plan.failed.length > 1 ? "s" : ""} qui n'a pas abouti : « ${f.source}/${f.tag} », commencée le ${when}${why}.`;
|
|
@@ -516,7 +513,7 @@ function describeDiscovery(facts, style) {
|
|
|
516
513
|
*/
|
|
517
514
|
function renderRefusal(verdict, message, style, ddl) {
|
|
518
515
|
const enMode = ddl ? refusalInMode(verdict.code, ddl, verdict.connector) : null;
|
|
519
|
-
const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code]
|
|
516
|
+
const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code];
|
|
520
517
|
const actions = enMode?.actions ?? verdict.nextActions;
|
|
521
518
|
return `${style.red(style.bold("Migration refusée"))} ${style.dim(`[${verdict.code}]`)}\n\n` + renderBlocks(style, message, meaning, actions);
|
|
522
519
|
}
|
|
@@ -98,8 +98,8 @@ async function readHistory(driver) {
|
|
|
98
98
|
finishedAt: row.finished_at === null ? null : Number(row.finished_at),
|
|
99
99
|
executionMs: row.execution_ms === null ? null : Number(row.execution_ms),
|
|
100
100
|
success: toBoolean(row.success),
|
|
101
|
-
error:
|
|
102
|
-
appliedBy:
|
|
101
|
+
error: toNullableText(row.error),
|
|
102
|
+
appliedBy: toNullableText(row.applied_by)
|
|
103
103
|
}));
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
@@ -194,6 +194,29 @@ async function forgetEntries(driver, entries) {
|
|
|
194
194
|
return removed;
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
|
+
* Décode une colonne texte NULLABLE de l'historique.
|
|
198
|
+
*
|
|
199
|
+
* Une colonne texte rend une chaîne ; un `Buffer` (collation binaire MySQL) est
|
|
200
|
+
* lu en UTF-8, comme le faisait `String()`. Un objet inattendu est sérialisé en
|
|
201
|
+
* JSON plutôt que réduit à `[object Object]`, qui effacerait la cause.
|
|
202
|
+
*
|
|
203
|
+
* @param value - valeur brute rendue par le driver.
|
|
204
|
+
* @returns le texte, ou `null` si la colonne est nulle.
|
|
205
|
+
*/
|
|
206
|
+
function toNullableText(value) {
|
|
207
|
+
if (value === null) return null;
|
|
208
|
+
switch (typeof value) {
|
|
209
|
+
case "string": return value;
|
|
210
|
+
case "number":
|
|
211
|
+
case "bigint":
|
|
212
|
+
case "boolean":
|
|
213
|
+
case "undefined": return String(value);
|
|
214
|
+
case "symbol": return value.toString();
|
|
215
|
+
case "function": return "function";
|
|
216
|
+
default: return Buffer.isBuffer(value) ? value.toString("utf8") : JSON.stringify(value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
197
220
|
* Normalise un booléen tel que le rend la base.
|
|
198
221
|
*
|
|
199
222
|
* PostgreSQL rend `true`, SQLite et MySQL rendent `1` — et un `1` textuel
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FORMAT_MARKER } from "./types.js";
|
|
2
|
-
import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed } from "./refusals.js";
|
|
2
|
+
import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, generationTopLevelAwait, introspectFailed } from "./refusals.js";
|
|
3
3
|
import { droppedTables, rebuiltTables } from "./destructive.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -69,8 +69,10 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
|
|
|
69
69
|
cwd,
|
|
70
70
|
encoding: "utf8"
|
|
71
71
|
});
|
|
72
|
-
const output =
|
|
72
|
+
const output = toolOutput(result);
|
|
73
73
|
if (result.status !== 0 || !generationHappened(output)) {
|
|
74
|
+
const awaited = topLevelAwaitFailure(output);
|
|
75
|
+
if (awaited !== null) throw new MigrationToolError(generationTopLevelAwait(label, awaited.file, awaited.line, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
|
|
74
76
|
if (isInteractivePromptFailure(output)) throw new MigrationToolError(generationNeedsAnswer(label, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
|
|
75
77
|
throw new MigrationToolError(generationFailed(label, result.status, output));
|
|
76
78
|
}
|
|
@@ -105,7 +107,7 @@ function runIntrospect({ cwd, configRel, label }) {
|
|
|
105
107
|
cwd,
|
|
106
108
|
encoding: "utf8"
|
|
107
109
|
});
|
|
108
|
-
const output =
|
|
110
|
+
const output = toolOutput(result);
|
|
109
111
|
if (result.status !== 0) throw new MigrationToolError(introspectFailed(label, result.status, output));
|
|
110
112
|
return output;
|
|
111
113
|
}
|
|
@@ -226,9 +228,9 @@ const BLOCKING_PATTERNS = [
|
|
|
226
228
|
id: "colonne-neuve-puis-index-unique",
|
|
227
229
|
pattern: /\bCREATE\s+UNIQUE\s+INDEX\b/i,
|
|
228
230
|
detect: (code) => {
|
|
229
|
-
const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].
|
|
231
|
+
const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
|
|
230
232
|
if (added.length === 0) return false;
|
|
231
|
-
return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some((
|
|
233
|
+
return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some(([, columns = ""]) => columns.split(",").map((c) => c.trim().replace(/[`"']/g, "").toLowerCase()).some((c) => added.includes(c)));
|
|
232
234
|
},
|
|
233
235
|
what: "ajoute une colonne ET pose son index UNIQUE dans la même migration : toutes les lignes déjà présentes reçoivent la même valeur, et l'index échoue sur la deuxième — cet enchaînement ne réussit que sur une table vide",
|
|
234
236
|
todo: "séparer : ajouter la colonne sans contrainte d'unicité, remplir chaque ligne d'une valeur DISTINCTE (`nodefony orm:generate --custom`), puis poser l'index unique"
|
|
@@ -273,6 +275,25 @@ function isInteractivePromptFailure(output) {
|
|
|
273
275
|
return /Interactive prompts require a TTY/i.test(output);
|
|
274
276
|
}
|
|
275
277
|
/**
|
|
278
|
+
* Reconnaît le refus de l'outil devant un `await` de premier niveau.
|
|
279
|
+
*
|
|
280
|
+
* C'est l'esbuild de `drizzle-kit` qui le détecte, en transpilant le fichier en
|
|
281
|
+
* CommonJS : sa ligne d'erreur nomme le fichier et la position. On la lit plutôt
|
|
282
|
+
* que d'analyser nous-mêmes les sources — l'outil qui refuse est aussi celui qui
|
|
283
|
+
* sait exactement où, sans faux positif. Fonction PURE, éprouvée sans process.
|
|
284
|
+
*
|
|
285
|
+
* @param output - sortie complète de l'outil.
|
|
286
|
+
* @returns le fichier et la ligne, ou `null` si l'échec est d'une autre nature.
|
|
287
|
+
*/
|
|
288
|
+
function topLevelAwaitFailure(output) {
|
|
289
|
+
const plain = output.replace(/\u001B\[[0-9;]*m/g, "");
|
|
290
|
+
const [, file, line] = /(?:^|\n)[^\S\n]*(\S[^\n]*?\.[cm]?[jt]sx?):(\d+):\d+: ERROR: Top-level await is currently not supported with the \\?"cjs\\?" output format/u.exec(plain) ?? [];
|
|
291
|
+
return file === void 0 ? null : {
|
|
292
|
+
file,
|
|
293
|
+
line: Number(line)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
276
297
|
* Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
|
|
277
298
|
*
|
|
278
299
|
* Écrit en fins de ligne `\n` quel que soit le système : le dépôt et le gabarit
|
|
@@ -295,5 +316,19 @@ function stampFormatMarker(dir) {
|
|
|
295
316
|
}
|
|
296
317
|
return stamped;
|
|
297
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Sortie complète (standard puis erreur) d'un `spawnSync`.
|
|
321
|
+
*
|
|
322
|
+
* Malgré leur type, `stdout` et `stderr` valent `null` quand le processus n'a
|
|
323
|
+
* pas pu démarrer (`ENOENT`, `EACCES`) : Node les tire alors d'un `output` nul.
|
|
324
|
+
*
|
|
325
|
+
* @param result - le retour de `spawnSync` en mode `encoding: "utf8"`.
|
|
326
|
+
* @returns les deux flux concaténés, vide si rien n'a été écrit.
|
|
327
|
+
*/
|
|
328
|
+
function toolOutput(result) {
|
|
329
|
+
const stdout = result.stdout;
|
|
330
|
+
const stderr = result.stderr;
|
|
331
|
+
return `${stdout ?? ""}${stderr ?? ""}`;
|
|
332
|
+
}
|
|
298
333
|
//#endregion
|
|
299
|
-
export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker };
|
|
334
|
+
export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker, topLevelAwaitFailure };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownsSharedMigrations } from "../frameworkConnector.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -15,6 +16,47 @@ const FRAMEWORK_RANK = 0;
|
|
|
15
16
|
* celles des modules ; l'inverse n'arrive jamais.
|
|
16
17
|
*/
|
|
17
18
|
const APP_RANK = 1e6;
|
|
19
|
+
/**
|
|
20
|
+
* Noms qui ne peuvent PAS désigner le dossier d'un connecteur secondaire : ce
|
|
21
|
+
* sont ceux des dossiers de dialecte (et du journal) que le connecteur du
|
|
22
|
+
* framework range déjà sous `migrations/`. Un connecteur `postgres` y lirait les
|
|
23
|
+
* migrations de `default` comme les siennes.
|
|
24
|
+
*/
|
|
25
|
+
const RESERVED_MIGRATION_DIRS = [
|
|
26
|
+
"sqlite",
|
|
27
|
+
"postgres",
|
|
28
|
+
"mysql",
|
|
29
|
+
"meta"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Le nom de ce connecteur collisionne-t-il avec un dossier de dialecte ?
|
|
33
|
+
*
|
|
34
|
+
* @param connector - nom du connecteur (clé de `connectors`).
|
|
35
|
+
* @returns `true` pour un connecteur SECONDAIRE nommé comme un dossier réservé.
|
|
36
|
+
*/
|
|
37
|
+
function isReservedConnectorName(connector) {
|
|
38
|
+
return !ownsSharedMigrations(connector) && RESERVED_MIGRATION_DIRS.includes(connector);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Dossier des migrations PROPRES à un connecteur — la seule règle qui dit où
|
|
42
|
+
* elles vivent.
|
|
43
|
+
*
|
|
44
|
+
* Le connecteur du framework garde la racine (`migrations/<dialecte>`) : ses
|
|
45
|
+
* migrations décrivent la base du framework et des entités de l'application.
|
|
46
|
+
* Un connecteur secondaire reçoit son sous-dossier (`migrations/<connecteur>/
|
|
47
|
+
* <dialecte>`) : sa base ne reçoit que ce qui y est écrit, et celle du
|
|
48
|
+
* framework n'en voit rien — le dossier de dialecte qu'elle lit n'est pas le
|
|
49
|
+
* même.
|
|
50
|
+
*
|
|
51
|
+
* @param appDir - dossier de migrations de l'application (`appMigrationsDir`).
|
|
52
|
+
* @param connector - nom du connecteur.
|
|
53
|
+
* @returns le dossier parent des sous-dossiers de dialecte, ou `undefined` sans
|
|
54
|
+
* racine d'application, ou pour un nom réservé ({@link isReservedConnectorName}).
|
|
55
|
+
*/
|
|
56
|
+
function connectorMigrationsDir(appDir, connector) {
|
|
57
|
+
if (appDir === void 0 || isReservedConnectorName(connector)) return;
|
|
58
|
+
return ownsSharedMigrations(connector) ? appDir : path.join(appDir, connector);
|
|
59
|
+
}
|
|
18
60
|
/** Dossier de migrations, mémoïsé — la remontée ne se fait qu'une fois. */
|
|
19
61
|
let cachedDir = null;
|
|
20
62
|
/**
|
|
@@ -68,10 +110,22 @@ async function frameworkMigrationsDir() {
|
|
|
68
110
|
*
|
|
69
111
|
* @param appDir - dossier de migrations de l'application, s'il y en a un.
|
|
70
112
|
* @param options.framework - `false` quand le module est data-only.
|
|
113
|
+
* @param options.connector - connecteur visé ; un connecteur secondaire ne
|
|
114
|
+
* reçoit que SES migrations ({@link connectorMigrationsDir}), jamais celles
|
|
115
|
+
* du framework. Omis : le registre complet (usage direct).
|
|
71
116
|
* @returns le registre, prêt pour l'applicateur.
|
|
72
117
|
*/
|
|
73
118
|
async function defaultMigrationSources(appDir, options = {}) {
|
|
74
119
|
const sources = [];
|
|
120
|
+
if (options.connector !== void 0 && !ownsSharedMigrations(options.connector)) {
|
|
121
|
+
const own = connectorMigrationsDir(appDir, options.connector);
|
|
122
|
+
if (own !== void 0) sources.push({
|
|
123
|
+
name: "app",
|
|
124
|
+
dir: own,
|
|
125
|
+
rank: APP_RANK
|
|
126
|
+
});
|
|
127
|
+
return sources;
|
|
128
|
+
}
|
|
75
129
|
if (options.framework !== false) sources.push({
|
|
76
130
|
name: FRAMEWORK_SOURCE,
|
|
77
131
|
dir: await frameworkMigrationsDir(),
|
|
@@ -85,4 +139,4 @@ async function defaultMigrationSources(appDir, options = {}) {
|
|
|
85
139
|
return sources;
|
|
86
140
|
}
|
|
87
141
|
//#endregion
|
|
88
|
-
export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, defaultMigrationSources, frameworkMigrationsDir };
|
|
142
|
+
export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, RESERVED_MIGRATION_DIRS, connectorMigrationsDir, defaultMigrationSources, frameworkMigrationsDir, isReservedConnectorName };
|
|
@@ -58,7 +58,6 @@ function repairRebuildCopy(sql, columnsOf) {
|
|
|
58
58
|
const groups = args[args.length - 1];
|
|
59
59
|
if (groups === void 0) return match;
|
|
60
60
|
const { head, into, middle, select, tail, source } = groups;
|
|
61
|
-
if (head === void 0 || into === void 0 || middle === void 0 || select === void 0 || tail === void 0 || source === void 0) return match;
|
|
62
61
|
const known = columnsOf(source);
|
|
63
62
|
if (known === null) return match;
|
|
64
63
|
const intoColumns = splitColumns(into);
|
|
@@ -68,13 +67,14 @@ function repairRebuildCopy(sql, columnsOf) {
|
|
|
68
67
|
const keptInto = [];
|
|
69
68
|
const keptSelect = [];
|
|
70
69
|
const removed = [];
|
|
71
|
-
for (
|
|
72
|
-
const
|
|
73
|
-
const
|
|
70
|
+
for (const [i, intoColumn] of intoColumns.entries()) {
|
|
71
|
+
const selectColumn = selectColumns[i] ?? "";
|
|
72
|
+
const target = bareName(intoColumn);
|
|
73
|
+
const read = bareName(selectColumn);
|
|
74
74
|
if (target === null || read === null) return match;
|
|
75
75
|
if (carried.has(read)) {
|
|
76
|
-
keptInto.push(
|
|
77
|
-
keptSelect.push(
|
|
76
|
+
keptInto.push(intoColumn);
|
|
77
|
+
keptSelect.push(selectColumn);
|
|
78
78
|
} else removed.push(`${source}.${read}`);
|
|
79
79
|
}
|
|
80
80
|
if (removed.length === 0) return match;
|