@nodefony/drizzle 10.0.0-alpha.9 → 10.0.0-beta.2
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 +28 -26
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +5 -5
- package/dist/nodefony/command/orm-generate.js +3 -3
- package/dist/nodefony/config/defineModuleConfig.js +2 -2
- package/dist/nodefony/entity/userTable.js +1 -1
- package/dist/nodefony/registerStores.js +2 -2
- package/dist/nodefony/service/DrizzleService.js +26 -12
- package/dist/nodefony/src/DrizzleAuditStore.js +1 -1
- package/dist/nodefony/src/connectorTarget.js +1 -1
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
- package/dist/nodefony/src/migrator/adopt.js +1 -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 +6 -3
- 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 +23 -9
- package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
- package/dist/nodefony/src/migrator/resolve.js +4 -4
- package/dist/nodefony/src/migrator/sources.js +13 -8
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +55 -32
- package/dist/nodefony/src/orm-core/DrizzleRepository.js +142 -40
- package/dist/nodefony/src/queryKit.js +5 -5
- package/dist/nodefony/src/sqliteJournal.js +54 -0
- package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
- package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
- 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/name.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +3 -3
- 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/dist/types/nodefony/src/sqliteJournal.d.ts +53 -0
- package/docs/index.md +50 -39
- package/docs/migrations.md +1 -1
- package/package.json +16 -16
|
@@ -30,12 +30,12 @@ import { sql } from "drizzle-orm";
|
|
|
30
30
|
*/
|
|
31
31
|
async function findUserIdBySocialProvider(db, dialect, provider, providerId) {
|
|
32
32
|
switch (dialect) {
|
|
33
|
-
case "sqlite": return
|
|
33
|
+
case "sqlite": return db.all(sql`SELECT "id" AS id FROM ${ident("sqlite", USER_TABLE_NAME)}
|
|
34
34
|
WHERE EXISTS (
|
|
35
35
|
SELECT 1 FROM json_each(${ident("sqlite", USER_TABLE_NAME)}."socialProviders")
|
|
36
36
|
WHERE json_extract(value, '$.provider') = ${provider}
|
|
37
37
|
AND json_extract(value, '$.providerId') = ${providerId}
|
|
38
|
-
) LIMIT 1`)
|
|
38
|
+
) LIMIT 1`)[0]?.id ?? null;
|
|
39
39
|
case "postgres": {
|
|
40
40
|
const pattern = JSON.stringify([{
|
|
41
41
|
provider,
|
|
@@ -95,7 +95,7 @@ async function reserveIdempotencyKeyMysql(db, args) {
|
|
|
95
95
|
const [inserted] = await exec.execute(sql`INSERT IGNORE INTO ${sql.raw("`idempotency_key`")}
|
|
96
96
|
(${sql.raw("`key`, `fingerprint`, `state`, `response`, `expiresAt`")})
|
|
97
97
|
VALUES (${key}, ${fingerprint}, 'if', NULL, ${leaseExpiresAt})`);
|
|
98
|
-
if ((inserted
|
|
98
|
+
if ((inserted.affectedRows ?? 0) === 1) return true;
|
|
99
99
|
const [stolen] = await exec.execute(sql`UPDATE ${sql.raw("`idempotency_key`")}
|
|
100
100
|
SET ${sql.raw("`fingerprint`")} = ${fingerprint},
|
|
101
101
|
${sql.raw("`state`")} = 'if',
|
|
@@ -103,7 +103,7 @@ async function reserveIdempotencyKeyMysql(db, args) {
|
|
|
103
103
|
${sql.raw("`expiresAt`")} = ${leaseExpiresAt}
|
|
104
104
|
WHERE ${sql.raw("`key`")} = ${key}
|
|
105
105
|
AND ${sql.raw("`expiresAt`")} < ${now}`);
|
|
106
|
-
return (stolen
|
|
106
|
+
return (stolen.affectedRows ?? 0) > 0;
|
|
107
107
|
}
|
|
108
108
|
/** Identifiant quoté selon le dialecte (`"x"` SQL standard, `` `x` `` MySQL). */
|
|
109
109
|
function ident(dialect, name) {
|
|
@@ -189,7 +189,7 @@ function orderBySql(dialect, order, allowed, fallback, tiebreaker) {
|
|
|
189
189
|
/** Exécute un SELECT et normalise le retour en tableau de lignes (API native divergente). */
|
|
190
190
|
async function runSelect(db, dialect, query) {
|
|
191
191
|
switch (dialect) {
|
|
192
|
-
case "sqlite": return
|
|
192
|
+
case "sqlite": return db.all(query);
|
|
193
193
|
case "postgres": return (await db.execute(query)).rows;
|
|
194
194
|
case "mysql": {
|
|
195
195
|
const [rows] = await db.execute(query);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region nodefony/src/sqliteJournal.ts
|
|
2
|
+
/** Échéance par défaut : au-delà, une base occupée l'est pour de bon. */
|
|
3
|
+
const WAL_DEADLINE_MS = 1e4;
|
|
4
|
+
const sleepFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
/**
|
|
6
|
+
* L'erreur est-elle un conflit de verrou, qui se règle en réessayant ?
|
|
7
|
+
*
|
|
8
|
+
* @param e - erreur levée par `better-sqlite3`
|
|
9
|
+
* @returns vrai pour `SQLITE_BUSY*` et `SQLITE_LOCKED*`
|
|
10
|
+
*/
|
|
11
|
+
function isSqliteBusy(e) {
|
|
12
|
+
const code = e?.code;
|
|
13
|
+
return typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Passe la base en WAL (`synchronous = NORMAL`), en tolérant les ouvertures
|
|
17
|
+
* CONCURRENTES d'une base neuve.
|
|
18
|
+
*
|
|
19
|
+
* Basculer en WAL exige un verrou exclusif. Deux processus qui ouvrent la même
|
|
20
|
+
* base neuve au même instant — l'application et `orm:migrate` lancé par
|
|
21
|
+
* `docker exec`, ou les workers de `nodefony cluster` — tiennent chacun un
|
|
22
|
+
* verrou partagé et veulent l'exclusif : SQLite tranche l'impasse par un
|
|
23
|
+
* `SQLITE_BUSY` IMMÉDIAT, sans passer par l'attente (`timeout`) de la
|
|
24
|
+
* connexion. Mesuré : 23 ouvertures sur 40 échouaient avec trois processus
|
|
25
|
+
* synchronisés, et la forge a vu un `orm:migrate` refusé au démarrage.
|
|
26
|
+
*
|
|
27
|
+
* Le mode WAL est PERSISTANT (écrit dans le fichier) : on le lit d'abord, et
|
|
28
|
+
* seul le premier ouvreur bascule. Un conflit se réessaie, à délai croissant,
|
|
29
|
+
* jusqu'à {@link WAL_DEADLINE_MS} ; toute autre erreur remonte telle quelle.
|
|
30
|
+
*
|
|
31
|
+
* @param db - connexion ouverte sur un FICHIER (sans objet sur `:memory:`)
|
|
32
|
+
* @param options - échéance, attente et horloge (injectées en test)
|
|
33
|
+
* @throws La dernière erreur `SQLITE_BUSY` si l'échéance est dépassée, ou
|
|
34
|
+
* toute autre erreur au premier essai
|
|
35
|
+
*/
|
|
36
|
+
async function enableWriteAheadLog(db, options = {}) {
|
|
37
|
+
const deadlineMs = options.deadlineMs ?? 1e4;
|
|
38
|
+
const sleep = options.sleep ?? sleepFor;
|
|
39
|
+
const now = options.now ?? Date.now;
|
|
40
|
+
const start = now();
|
|
41
|
+
for (let attempt = 1;; attempt++) try {
|
|
42
|
+
if (db.pragma("journal_mode", { simple: true }) !== "wal") {
|
|
43
|
+
const mode = db.pragma("journal_mode = WAL", { simple: true });
|
|
44
|
+
if (mode !== "wal") throw Object.assign(/* @__PURE__ */ new Error(`journal_mode resté « ${String(mode)} »`), { code: "SQLITE_BUSY" });
|
|
45
|
+
}
|
|
46
|
+
db.pragma("synchronous = NORMAL");
|
|
47
|
+
return;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (!isSqliteBusy(e) || now() - start >= deadlineMs) throw e;
|
|
50
|
+
await sleep(Math.min(20 * attempt, 250));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { WAL_DEADLINE_MS, enableWriteAheadLog, isSqliteBusy };
|
|
@@ -40,7 +40,7 @@ export declare class DrizzleUserRepository implements IUserRepository {
|
|
|
40
40
|
updateMany(criteria: Criteria<IPasswordAuthenticatedUser>, data: Partial<IPasswordAuthenticatedUser>): Promise<number>;
|
|
41
41
|
delete(criteria: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
42
42
|
count(criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
43
|
-
countDistinct(field: keyof IPasswordAuthenticatedUser
|
|
43
|
+
countDistinct(field: Extract<keyof IPasswordAuthenticatedUser, string>, criteria?: Criteria<IPasswordAuthenticatedUser>): Promise<number>;
|
|
44
44
|
withTransaction(tx: ITransaction): IUserRepository;
|
|
45
45
|
findByIdentifier(identifier: string): Promise<IPasswordAuthenticatedUser | null>;
|
|
46
46
|
/**
|
|
@@ -35,7 +35,7 @@ export declare function defaultConnectorFilename(kernel: Kernel | null, name: st
|
|
|
35
35
|
export interface IConnectorTarget {
|
|
36
36
|
dialect: SqlDialect;
|
|
37
37
|
filename?: string;
|
|
38
|
-
url?: string;
|
|
38
|
+
url?: string | undefined;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Résout les coordonnées d'un connecteur : dialecte, et fichier ou URL.
|
|
@@ -10,7 +10,7 @@ export interface IDrizzleMigratorOptions extends IMigrationTarget {
|
|
|
10
10
|
/** Registre de sources, espace de noms OUVERT. */
|
|
11
11
|
sources: readonly IMigrationSource[];
|
|
12
12
|
/** Délai d'attente du verrou (ms). */
|
|
13
|
-
lockTimeoutMs?: number;
|
|
13
|
+
lockTimeoutMs?: number | undefined;
|
|
14
14
|
/** Qui applique — hôte du job par défaut. */
|
|
15
15
|
appliedBy?: string;
|
|
16
16
|
/** Horloge, injectable pour les tests. */
|
|
@@ -71,12 +71,12 @@ export declare class DrizzleMigrator {
|
|
|
71
71
|
* @returns ce qui a été levé et ré-aligné.
|
|
72
72
|
*/
|
|
73
73
|
repair(options?: {
|
|
74
|
-
source?: string;
|
|
75
|
-
updateHashes?: boolean;
|
|
74
|
+
source?: string | undefined;
|
|
75
|
+
updateHashes?: boolean | undefined;
|
|
76
76
|
forget?: readonly {
|
|
77
77
|
source: string;
|
|
78
78
|
tag: string;
|
|
79
|
-
}[];
|
|
79
|
+
}[] | undefined;
|
|
80
80
|
}): Promise<{
|
|
81
81
|
cleared: {
|
|
82
82
|
source: string;
|
|
@@ -8,9 +8,9 @@ export interface IMigrationTarget {
|
|
|
8
8
|
/** Dialecte du connecteur. */
|
|
9
9
|
dialect: SqlDialect;
|
|
10
10
|
/** Fichier SQLite (dialecte `sqlite`). */
|
|
11
|
-
filename?: string;
|
|
11
|
+
filename?: string | undefined;
|
|
12
12
|
/** URL de connexion DIRECTE (dialectes `postgres` et `mysql`). */
|
|
13
|
-
url?: string;
|
|
13
|
+
url?: string | undefined;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Ouvre le pilote à connexion unique du dialecte demandé.
|
|
@@ -108,15 +108,15 @@ export interface IMigrationEntryReport {
|
|
|
108
108
|
/** Où en est cette migration pour CE connecteur. */
|
|
109
109
|
status: "applied" | "pending" | "failed" | "drifted" | "missing";
|
|
110
110
|
/** Début de l'application, en millisecondes — absent si jamais appliquée. */
|
|
111
|
-
appliedAt?: number;
|
|
111
|
+
appliedAt?: number | undefined;
|
|
112
112
|
/** Durée de l'application, en millisecondes. */
|
|
113
|
-
durationMs?: number;
|
|
113
|
+
durationMs?: number | undefined;
|
|
114
114
|
/** Ce qui l'a appliquée, tel que l'historique l'a retenu. */
|
|
115
|
-
appliedBy?: string;
|
|
115
|
+
appliedBy?: string | undefined;
|
|
116
116
|
/** Déploiement qui l'a portée — groupe les migrations d'un même passage. */
|
|
117
|
-
runId?: string;
|
|
117
|
+
runId?: string | undefined;
|
|
118
118
|
/** Motif de l'échec, quand elle a échoué. */
|
|
119
|
-
error?: string;
|
|
119
|
+
error?: string | undefined;
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
122
|
* La charge utile figée — cœur NEUTRE au premier niveau, spécifique du pilote
|
|
@@ -155,7 +155,7 @@ export interface IMigrationReport {
|
|
|
155
155
|
* Son ABSENCE est un fait, pas un oubli : sur une base conforme, il n'y a
|
|
156
156
|
* rien à nommer.
|
|
157
157
|
*/
|
|
158
|
-
divergence?: ISchemaComparison;
|
|
158
|
+
divergence?: ISchemaComparison | undefined;
|
|
159
159
|
/**
|
|
160
160
|
* Ce que le passage a DÉTRUIT, nommé — présent au seul run réel qui a
|
|
161
161
|
* appliqué une migration destructive, absent partout ailleurs.
|
|
@@ -166,7 +166,7 @@ export interface IMigrationReport {
|
|
|
166
166
|
* octet de ce qu'il relit ne la mentionne. Son absence est un fait : rien
|
|
167
167
|
* n'a été détruit.
|
|
168
168
|
*/
|
|
169
|
-
destructive?: IDestructiveFinding[];
|
|
169
|
+
destructive?: IDestructiveFinding[] | undefined;
|
|
170
170
|
/** Tout ce qui est propre au pilote SQL vit ici, et nulle part ailleurs. */
|
|
171
171
|
driver: {
|
|
172
172
|
kind: "sql";
|
|
@@ -174,9 +174,9 @@ export interface IMigrationReport {
|
|
|
174
174
|
ddl: string;
|
|
175
175
|
historyTable: string;
|
|
176
176
|
/** La base visée, sans identifiant ni mot de passe. */
|
|
177
|
-
target?: string;
|
|
177
|
+
target?: string | undefined;
|
|
178
178
|
/** `true` quand la cible vient de `NF_MIGRATE_DATABASE_URL`. */
|
|
179
|
-
fromMigrateUrl?: boolean;
|
|
179
|
+
fromMigrateUrl?: boolean | undefined;
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
/** Grille des codes de sortie — **figée, jamais réaffectée**. */
|
|
@@ -34,7 +34,7 @@ export type MigrationNameCheck = {
|
|
|
34
34
|
* geste qui échoue est pire que ne rien proposer, parce qu'il fait perdre
|
|
35
35
|
* un aller-retour ET la confiance dans les autres suggestions.
|
|
36
36
|
*/
|
|
37
|
-
suggestion?: string;
|
|
37
|
+
suggestion?: string | undefined;
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
40
|
* Dérive un nom acceptable d'une saisie qui ne l'est pas.
|
|
@@ -98,9 +98,9 @@ export interface IMigrationVerdict {
|
|
|
98
98
|
/** Connecteur concerné. */
|
|
99
99
|
connector: string;
|
|
100
100
|
/** Source de migrations concernée, quand le refus en désigne une. */
|
|
101
|
-
source?: string;
|
|
101
|
+
source?: string | undefined;
|
|
102
102
|
/** Tag concerné, quand le refus en désigne un. */
|
|
103
|
-
tag?: string;
|
|
103
|
+
tag?: string | undefined;
|
|
104
104
|
/** Faits constatés, sans mise en forme — de quoi rendre la phrase. */
|
|
105
105
|
facts: Record<string, string | number | boolean | readonly string[]>;
|
|
106
106
|
/** Ce qu'il faut faire ensuite, du plus direct au plus assumé. */
|
|
@@ -13,12 +13,12 @@ export interface DrizzleOrmOptions {
|
|
|
13
13
|
*/
|
|
14
14
|
dialect?: SqlDialect;
|
|
15
15
|
/** Fichier SQLite (`":memory:"` par défaut) — dialecte `sqlite` uniquement. */
|
|
16
|
-
filename?: string;
|
|
16
|
+
filename?: string | undefined;
|
|
17
17
|
/**
|
|
18
18
|
* Chaîne de connexion (`postgres://…`, `mysql://…`) — dialectes `postgres`/
|
|
19
19
|
* `mysql`. Requise pour ces dialectes.
|
|
20
20
|
*/
|
|
21
|
-
url?: string;
|
|
21
|
+
url?: string | undefined;
|
|
22
22
|
/**
|
|
23
23
|
* Container du service qui construit l'ORM — il porte le `syslog` du kernel.
|
|
24
24
|
*
|
|
@@ -28,7 +28,7 @@ export interface DrizzleOrmOptions {
|
|
|
28
28
|
* échecs des sondes, jamais le diagnostic de la reprise. Omis en usage
|
|
29
29
|
* direct (bancs de test) : l'ORM reste alors autonome, comme avant.
|
|
30
30
|
*/
|
|
31
|
-
container?: Container;
|
|
31
|
+
container?: Container | undefined;
|
|
32
32
|
/**
|
|
33
33
|
* Le schéma doit-il être DÉRIVÉ du code à la connexion ?
|
|
34
34
|
*
|
|
@@ -15,7 +15,7 @@ import type { Criteria, IRepository, ITransaction, RepositoryReadOptions, Update
|
|
|
15
15
|
* La surface d'exécution native qui DIVERGE (`db.all` vs `db.execute().rows`)
|
|
16
16
|
* est routée par le `queryKit` — jamais ici.
|
|
17
17
|
*/
|
|
18
|
-
export type DrizzleDb = BetterSQLite3Database
|
|
18
|
+
export type DrizzleDb = BetterSQLite3Database;
|
|
19
19
|
/** Table Drizzle multi-dialecte (l'union honnête des variantes colKit). */
|
|
20
20
|
export type DrizzleTable = SQLiteTable | PgTable | MySqlTable;
|
|
21
21
|
/** Colonne Drizzle dialecte-agnostique (classe de base — acceptée par les
|
|
@@ -24,9 +24,9 @@ export interface IResolvedForeignKey {
|
|
|
24
24
|
/** Colonnes visées, dans le même ordre que {@link columns}. */
|
|
25
25
|
readonly foreignColumns: readonly string[];
|
|
26
26
|
/** Politique d'effacement du parent (`restrict`, `set null`, `cascade`…). */
|
|
27
|
-
readonly onDelete?: string;
|
|
27
|
+
readonly onDelete?: string | undefined;
|
|
28
28
|
/** Politique de mise à jour de la clé du parent. */
|
|
29
|
-
readonly onUpdate?: string;
|
|
29
|
+
readonly onUpdate?: string | undefined;
|
|
30
30
|
}
|
|
31
31
|
/** Une table à créer, et les tables que ses clés étrangères désignent. */
|
|
32
32
|
export interface ITableDependency {
|
|
@@ -66,11 +66,11 @@ export declare function reserveIdempotencyKeyMysql(db: DrizzleDb, args: {
|
|
|
66
66
|
}): Promise<boolean>;
|
|
67
67
|
/** Filtres du listing utilisateur (sous-ensemble non portable de `IUserListQuery`). */
|
|
68
68
|
export interface UserListFilters {
|
|
69
|
-
role?: string;
|
|
70
|
-
enabled?: boolean;
|
|
71
|
-
locked?: boolean;
|
|
72
|
-
hasSocial?: boolean;
|
|
73
|
-
q?: string;
|
|
69
|
+
role?: string | undefined;
|
|
70
|
+
enabled?: boolean | undefined;
|
|
71
|
+
locked?: boolean | undefined;
|
|
72
|
+
hasSocial?: boolean | undefined;
|
|
73
|
+
q?: string | undefined;
|
|
74
74
|
}
|
|
75
75
|
/** Fenêtre + tri résolus (le défaut `identifier ASC` est posé par l'appelant). */
|
|
76
76
|
export interface UserPageWindow {
|
|
@@ -91,10 +91,10 @@ export declare function listUserIdsPage(db: DrizzleDb, dialect: SqlDialect, filt
|
|
|
91
91
|
}>;
|
|
92
92
|
/** Filtres du listing d'endpoints (sous-ensemble non portable d'`IWebhookListQuery`). */
|
|
93
93
|
export interface WebhookListFilters {
|
|
94
|
-
enabled?: boolean;
|
|
95
|
-
event?: string;
|
|
96
|
-
failing?: boolean;
|
|
97
|
-
q?: string;
|
|
94
|
+
enabled?: boolean | undefined;
|
|
95
|
+
event?: string | undefined;
|
|
96
|
+
failing?: boolean | undefined;
|
|
97
|
+
q?: string | undefined;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
100
|
* Sélectionne les `id` d'une **page** d'endpoints webhook (filtres + tri +
|
|
@@ -37,6 +37,6 @@ import type { SqlDialect } from "../config/config.js";
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function describeTargetSafely(target: {
|
|
39
39
|
dialect: SqlDialect;
|
|
40
|
-
filename?: string;
|
|
41
|
-
url?: string;
|
|
40
|
+
filename?: string | undefined;
|
|
41
|
+
url?: string | undefined;
|
|
42
42
|
}, cwd?: string, grammar?: typeof path): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mise en WAL d'une base SQLite — UNE implémentation, appelée par les deux
|
|
3
|
+
* ouvreurs de fichier du paquet : l'adapter ORM (`DrizzleOrm`) et le pilote de
|
|
4
|
+
* migration (`sqliteDriver`). Une base migrée puis ouverte par l'application ne
|
|
5
|
+
* doit pas changer de mode en route, et les deux doivent survivre à la même
|
|
6
|
+
* concurrence.
|
|
7
|
+
*/
|
|
8
|
+
/** Ce que la mise en WAL lit d'une connexion `better-sqlite3`. */
|
|
9
|
+
export interface ISqlitePragmaClient {
|
|
10
|
+
pragma(source: string, options?: {
|
|
11
|
+
simple?: boolean;
|
|
12
|
+
}): unknown;
|
|
13
|
+
}
|
|
14
|
+
/** Réglages de la mise en WAL — injectables pour l'éprouver sans attendre. */
|
|
15
|
+
export interface IWriteAheadLogOptions {
|
|
16
|
+
/** Durée maximale des réessais, en millisecondes. */
|
|
17
|
+
deadlineMs?: number;
|
|
18
|
+
/** Attente entre deux essais. */
|
|
19
|
+
sleep?: (ms: number) => Promise<void>;
|
|
20
|
+
/** Horloge (millisecondes). */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
/** Échéance par défaut : au-delà, une base occupée l'est pour de bon. */
|
|
24
|
+
export declare const WAL_DEADLINE_MS = 10000;
|
|
25
|
+
/**
|
|
26
|
+
* L'erreur est-elle un conflit de verrou, qui se règle en réessayant ?
|
|
27
|
+
*
|
|
28
|
+
* @param e - erreur levée par `better-sqlite3`
|
|
29
|
+
* @returns vrai pour `SQLITE_BUSY*` et `SQLITE_LOCKED*`
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSqliteBusy(e: unknown): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Passe la base en WAL (`synchronous = NORMAL`), en tolérant les ouvertures
|
|
34
|
+
* CONCURRENTES d'une base neuve.
|
|
35
|
+
*
|
|
36
|
+
* Basculer en WAL exige un verrou exclusif. Deux processus qui ouvrent la même
|
|
37
|
+
* base neuve au même instant — l'application et `orm:migrate` lancé par
|
|
38
|
+
* `docker exec`, ou les workers de `nodefony cluster` — tiennent chacun un
|
|
39
|
+
* verrou partagé et veulent l'exclusif : SQLite tranche l'impasse par un
|
|
40
|
+
* `SQLITE_BUSY` IMMÉDIAT, sans passer par l'attente (`timeout`) de la
|
|
41
|
+
* connexion. Mesuré : 23 ouvertures sur 40 échouaient avec trois processus
|
|
42
|
+
* synchronisés, et la forge a vu un `orm:migrate` refusé au démarrage.
|
|
43
|
+
*
|
|
44
|
+
* Le mode WAL est PERSISTANT (écrit dans le fichier) : on le lit d'abord, et
|
|
45
|
+
* seul le premier ouvreur bascule. Un conflit se réessaie, à délai croissant,
|
|
46
|
+
* jusqu'à {@link WAL_DEADLINE_MS} ; toute autre erreur remonte telle quelle.
|
|
47
|
+
*
|
|
48
|
+
* @param db - connexion ouverte sur un FICHIER (sans objet sur `:memory:`)
|
|
49
|
+
* @param options - échéance, attente et horloge (injectées en test)
|
|
50
|
+
* @throws La dernière erreur `SQLITE_BUSY` si l'échéance est dépassée, ou
|
|
51
|
+
* toute autre erreur au premier essai
|
|
52
|
+
*/
|
|
53
|
+
export declare function enableWriteAheadLog(db: ISqlitePragmaClient, options?: IWriteAheadLogOptions): Promise<void>;
|
package/docs/index.md
CHANGED
|
@@ -200,7 +200,7 @@ Le tableau pour situer en cinq secondes ; les cards en dessous pour savoir où l
|
|
|
200
200
|
"desc": "Ce qui reste identique (les noms de colonnes, le contrat du repository) et ce qui diverge, avec la raison : types epoch/date/JSON, absence de `RETURNING` en MySQL, `OFFSET` sans `LIMIT`.",
|
|
201
201
|
"meta": "à lire avant de changer de base, pas après le premier incident" },
|
|
202
202
|
{ "icon": "🚧", "title": "migrations", "href": "#migrations--deux-chemins-un-seul-schéma",
|
|
203
|
-
"desc": "En développement, les tables sont créées au démarrage par un DDL dérivé de tes schémas
|
|
203
|
+
"desc": "En développement, les tables sont créées au démarrage par un DDL dérivé de tes schémas : index et clés étrangères compris, aucun `DEFAULT` SQL, et seules les colonnes facultatives ajoutées sont rattrapées. La section dit ce que ça implique en production.",
|
|
204
204
|
"meta": "le point à ne pas rater avant le premier déploiement" }
|
|
205
205
|
]
|
|
206
206
|
```
|
|
@@ -353,19 +353,24 @@ importable et testable sans serveur.
|
|
|
353
353
|
|
|
354
354
|
### Les options
|
|
355
355
|
|
|
356
|
-
| Option
|
|
357
|
-
|
|
|
358
|
-
| `connectors`
|
|
359
|
-
| `connectors.<n>.dialect`
|
|
360
|
-
| `connectors.<n>.filename`
|
|
361
|
-
| `connectors.<n>.url`
|
|
362
|
-
| `
|
|
356
|
+
| Option | Type | Défaut | Effet |
|
|
357
|
+
| -------------------------- | ----------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
358
|
+
| `connectors` | `Record<string, Connector>` | `{ default: {…} }` | Les connexions, indexées par nom (= clé du registre des ORM). |
|
|
359
|
+
| `connectors.<n>.dialect` | `"sqlite" \| "postgres" \| "mysql"` | `"sqlite"` | Choisit le driver (`better-sqlite3` / `pg` / `mysql2`). |
|
|
360
|
+
| `connectors.<n>.filename` | `string` | _résolu au boot_ | Fichier SQLite. `:memory:` = base éphémère. Ignoré hors `sqlite`. |
|
|
361
|
+
| `connectors.<n>.url` | `string` | — | Chaîne de connexion `postgres://…` / `mysql://…`. **Porte un secret.** |
|
|
362
|
+
| `connectors.<n>.ddl` | `"auto" \| "migrate" \| "none"` | _par environnement_ | `auto` en développement et en test (`migrate` dès que l'app versionne une migration), `none` ailleurs. |
|
|
363
|
+
| `migrations.dir` | `string` | `"migrations"` | Dossier des migrations versionnées. |
|
|
364
|
+
| `migrations.check` | `"fail" \| "warn" \| "off"` | _par environnement_ | Schéma en retard : `fail` retient `/readyz` (production), `warn` ailleurs. |
|
|
365
|
+
| `migrations.lockTimeoutMs` | `number` | `30000` | Attente du verrou de migration entre exemplaires. |
|
|
366
|
+
| `migrations.divergence` | `"report" \| "fail" \| "off"` | `"report"` | Conduite face à une migration appliquée qui a changé depuis. |
|
|
367
|
+
| `frameworkEntities` | `boolean` | `true` | Déclare (ou non) le schéma des huit briques durables sur `default`. |
|
|
363
368
|
|
|
364
369
|
Table dérivée de `drizzleConfigSchema` (`config.ts:136`) et de `SQL_DIALECTS` (`config.ts:37`).
|
|
365
370
|
|
|
366
371
|
**`filename` est volontairement sans défaut.** Le chemin dépend du kernel, qui n'existe pas quand le
|
|
367
372
|
schéma est évalué. Il est résolu **au démarrage** par `DrizzleService.#defaultFilename()`
|
|
368
|
-
(`DrizzleService.ts:
|
|
373
|
+
(`DrizzleService.ts:230`) vers `<app>/var/databases/nodefony-<connecteur>.db` — sous `var/`, le dossier
|
|
369
374
|
commun des données runtime : « où sont mes données ? » a une réponse unique, un seul chemin à
|
|
370
375
|
sauvegarder et à ignorer dans git.
|
|
371
376
|
|
|
@@ -406,13 +411,13 @@ use("@nodefony/drizzle", {
|
|
|
406
411
|
|
|
407
412
|
> [!WARNING]
|
|
408
413
|
> Une URL de connexion **porte un mot de passe**. Le module ne la journalise jamais telle quelle :
|
|
409
|
-
> `redactUrl()` (`DrizzleService.ts:
|
|
414
|
+
> `redactUrl()` (`DrizzleService.ts:99`) remplace le mot de passe par `***` avant tout log de
|
|
410
415
|
> démarrage, et la sonde d'administration applique la même règle.
|
|
411
416
|
|
|
412
417
|
### Quand la connexion échoue, le démarrage échoue
|
|
413
418
|
|
|
414
419
|
Un connecteur **déclaré** qui ne se connecte pas lève une `BootConfigurationError`
|
|
415
|
-
(`DrizzleService.ts:
|
|
420
|
+
(`DrizzleService.ts:368`) — en développement **comme** en production. Ce n'est pas une sévérité
|
|
416
421
|
gratuite : une infrastructure déclarée mais injoignable ne se répare pas en continuant. Un serveur qui
|
|
417
422
|
démarrerait « vivant » avec ses stores morts accepterait des requêtes pour échouer plus tard, la cause
|
|
418
423
|
noyée dans un avertissement. Le message d'erreur nomme le connecteur, le dialecte, la cible rédigée et
|
|
@@ -420,20 +425,21 @@ la piste à vérifier.
|
|
|
420
425
|
|
|
421
426
|
Pour les dialectes réseau, la connexion fait un **ping réel** au démarrage : les pools `pg` et `mysql2`
|
|
422
427
|
sont paresseux, sans ce `SELECT 1` une base morte « se connecterait » et n'échouerait qu'à la première
|
|
423
|
-
requête métier (`#connectPostgres()`, `DrizzleOrm.ts:
|
|
428
|
+
requête métier (`#connectPostgres()`, `DrizzleOrm.ts:1206` · `#connectMysql()`, `DrizzleOrm.ts:1521`).
|
|
424
429
|
|
|
425
430
|
## Dialectes — une base par déploiement, un seul code
|
|
426
431
|
|
|
427
432
|
Trois dialectes au même contrat. Le tableau dit ce qu'il faut installer et ce qu'on obtient.
|
|
428
433
|
|
|
429
|
-
| Dialecte | Driver | Statut du paquet
|
|
430
|
-
| ---------- | ---------------- |
|
|
431
|
-
| `sqlite` | `better-sqlite3` |
|
|
432
|
-
| `postgres` | `pg` | dépendance **optionnelle** | production multi-pod (recommandé) | `url` |
|
|
433
|
-
| `mysql` | `mysql2` | dépendance **optionnelle** | production — **MySQL 8.4 et MariaDB 11.4** | `url` |
|
|
434
|
+
| Dialecte | Driver | Statut du paquet | Cible | Configuration |
|
|
435
|
+
| ---------- | ---------------- | ---------------------------------- | ------------------------------------------ | ------------- |
|
|
436
|
+
| `sqlite` | `better-sqlite3` | dépendance de pair **optionnelle** | développement, tests, production mono-nœud | `filename` |
|
|
437
|
+
| `postgres` | `pg` | dépendance de pair **optionnelle** | production multi-pod (recommandé) | `url` |
|
|
438
|
+
| `mysql` | `mysql2` | dépendance de pair **optionnelle** | production — **MySQL 8.4 et MariaDB 11.4** | `url` |
|
|
434
439
|
|
|
435
|
-
Les drivers
|
|
436
|
-
|
|
440
|
+
Les trois drivers sont des dépendances de pair optionnelles, posées par le scaffold pour le dialecte
|
|
441
|
+
choisi et chargées **paresseusement** à la connexion : une application n'installe, et ne charge, que
|
|
442
|
+
le pilote de son dialecte.
|
|
437
443
|
|
|
438
444
|
### Ce qui ne change pas
|
|
439
445
|
|
|
@@ -467,7 +473,7 @@ En MySQL, les verbes « qui rendent la ligne écrite » (`create`, `updateOne`,
|
|
|
467
473
|
`findOneAndDelete`) se décomposent en sélection de la cible → mutation bornée par la clé primaire **avec
|
|
468
474
|
le critère revérifié dans le `WHERE`** → relecture. Deux à trois allers-retours au lieu d'un : c'est le
|
|
469
475
|
prix du dialecte, payé **uniquement** en MySQL. Une course perdue rend `null`, jamais une mutation hors
|
|
470
|
-
critère (`#mysqlInsertReturning()`, `DrizzleRepository.ts:
|
|
476
|
+
critère (`#mysqlInsertReturning()`, `DrizzleRepository.ts:1228`).
|
|
471
477
|
|
|
472
478
|
Le SQL brut nécessaire aux entités du framework est lui aussi routé par dialecte, dans un seul fichier
|
|
473
479
|
(`queryKit.ts`) : recherche dans une colonne JSON (`findUserIdBySocialProvider()`, `queryKit.ts:76`),
|
|
@@ -507,7 +513,7 @@ connexion**, donc leurs tables sont créées au moment où l'ORM s'ouvre.
|
|
|
507
513
|
| Pièce | Rôle | Ancre |
|
|
508
514
|
| ---------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
509
515
|
| `Drizzle` (le module) | valide la config, déclare le schéma framework, monte le data plane | `index.ts` du module |
|
|
510
|
-
| `DrizzleService` | ouvre un ORM par connecteur au boot, ferme tout à l'arrêt | `connectAll()`, `DrizzleService.ts:
|
|
516
|
+
| `DrizzleService` | ouvre un ORM par connecteur au boot, ferme tout à l'arrêt | `connectAll()`, `DrizzleService.ts:214` |
|
|
511
517
|
| `DrizzleOrm` | la connexion : DDL dérivé, repositories, transactions, sonde | `DrizzleOrm.ts:214` |
|
|
512
518
|
| `DrizzleRepository<T>` | le CRUD portable, les opérateurs riches, l'eager-load | `DrizzleRepository.ts:146` |
|
|
513
519
|
| `DrizzleTransaction` | `BEGIN`/`COMMIT`/`ROLLBACK` pilotés à la main, sur les trois dialectes | `DrizzleTransaction.ts:70` |
|
|
@@ -518,12 +524,17 @@ connexion**, donc leurs tables sont créées au moment où l'ORM s'ouvre.
|
|
|
518
524
|
### Le DDL dérivé — comment les tables apparaissent
|
|
519
525
|
|
|
520
526
|
Drizzle ne « synchronise » pas un schéma. L'adapter dérive lui-même un `CREATE TABLE IF NOT EXISTS`
|
|
521
|
-
depuis chaque table déclarée (`#buildCreateTable()`, `DrizzleOrm.ts:509`) et l'exécute à la connexion
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
depuis chaque table déclarée (`#buildCreateTable()`, `DrizzleOrm.ts:509`) et l'exécute à la connexion
|
|
528
|
+
**quand le connecteur est en `ddl: "auto"`** — le défaut en développement et en test ; en production
|
|
529
|
+
le défaut est `none`, et un travail extérieur lance `orm:migrate`. Trois conséquences à connaître
|
|
530
|
+
**avant** de dépendre de ce mécanisme :
|
|
531
|
+
|
|
532
|
+
1. il **crée** les tables, leurs index et leurs clés étrangères ; il ne rattrape ensuite que les
|
|
533
|
+
colonnes ajoutées qui **acceptent le vide** — jamais une colonne obligatoire, jamais une
|
|
534
|
+
suppression ;
|
|
535
|
+
2. il n'émet **aucun `DEFAULT` SQL** — d'où la règle des défauts en `$defaultFn` ;
|
|
536
|
+
3. pour tout le reste (renommer, rendre obligatoire, supprimer), c'est une migration
|
|
537
|
+
(`orm:generate`, `orm:migrate`).
|
|
527
538
|
|
|
528
539
|
C'est un confort de développement, pas un outil de migration. La suite est dans
|
|
529
540
|
[Migrations](#migrations--deux-chemins-un-seul-schéma).
|
|
@@ -559,7 +570,7 @@ await posts.count({ views: { $gte: 10 } });
|
|
|
559
570
|
|
|
560
571
|
Les opérateurs (`$eq $ne $gt $gte $lt $lte $in $nin $like`) sont **ceux d'orm-core**, identiques sur
|
|
561
572
|
tous les drivers ; la traduction en `eq()`/`inArray()` se fait dans `#where()`
|
|
562
|
-
(`DrizzleRepository.ts:
|
|
573
|
+
(`DrizzleRepository.ts:521`). Leur référence complète est dans
|
|
563
574
|
[la page d'orm-core](../../orm-core/docs/index.md).
|
|
564
575
|
|
|
565
576
|
`$like` est émis avec sa clause `ESCAPE '\'` (`likeSql.ts`), ce qui rend un `%` ou un `_` **littéral**
|
|
@@ -570,11 +581,12 @@ lui-même en SQLite : le même critère ne rendait pas les mêmes lignes selon l
|
|
|
570
581
|
Deux points de comportement qui évitent des surprises :
|
|
571
582
|
|
|
572
583
|
- **« au plus une ligne »** est garanti par construction pour `updateOne`/`deleteOne`/`increment` :
|
|
573
|
-
la mutation est bornée
|
|
574
|
-
`
|
|
584
|
+
la mutation est bornée à une ligne : `WHERE <pk> = ?` quand le critère fixe toute la clé primaire,
|
|
585
|
+
sinon `WHERE pk IN (SELECT pk FROM (SELECT pk … LIMIT 1) AS picked)` — jamais un `LIMIT` sur
|
|
586
|
+
l'`UPDATE` lui-même (`#pickOne()`, `DrizzleRepository.ts:324`). C'est ce qui rend ces verbes portables — MySQL
|
|
575
587
|
interdit la forme naïve.
|
|
576
588
|
- **l'eager-load est manuel** : une requête `IN (…)` par relation déclarée, puis regroupement en
|
|
577
|
-
mémoire (`#populate()`, `DrizzleRepository.ts:
|
|
589
|
+
mémoire (`#populate()`, `DrizzleRepository.ts:817`). Choix assumé — pas de couche de relations à
|
|
578
590
|
déclarer une seconde fois, et le comportement est le même sur les trois dialectes.
|
|
579
591
|
|
|
580
592
|
### Transactions — une connexion dédiée, jamais le pool
|
|
@@ -627,7 +639,7 @@ const rows = await db.all(sql`
|
|
|
627
639
|
`);
|
|
628
640
|
```
|
|
629
641
|
|
|
630
|
-
C'est l'**anti-blocage** du modèle Repository (`getNativeConnection()`, `DrizzleOrm.ts:
|
|
642
|
+
C'est l'**anti-blocage** du modèle Repository (`getNativeConnection()`, `DrizzleOrm.ts:1792`) : CTE,
|
|
631
643
|
fonctions de fenêtre, sous-requêtes corrélées, jointures arbitraires. Deux contreparties assumées :
|
|
632
644
|
ce SQL n'est plus portable entre dialectes, et il **ne passe pas** par la sonde de profilage des
|
|
633
645
|
requêtes.
|
|
@@ -718,7 +730,7 @@ le framework résout ses stores avant que l'ORM ne soit connecté — et parce q
|
|
|
718
730
|
l'arrêt** avant que les serveurs HTTP n'aient fini de vider leurs requêtes en vol. Handle absent =
|
|
719
731
|
dégradation annoncée, pas un plantage : le `SessionStorage` rend une session vide et ignore les
|
|
720
732
|
écritures (`#repo()`, `SessionStorage.ts:86`), le store d'idempotence laisse passer la mutation sans
|
|
721
|
-
dédup (`begin()`, `DrizzleIdempotencyStore.ts:
|
|
733
|
+
dédup (`begin()`, `DrizzleIdempotencyStore.ts:225`).
|
|
722
734
|
|
|
723
735
|
**SQL n'a pas de TTL.** Contrairement à Redis, rien n'expire tout seul : chaque store expose un `gc()`
|
|
724
736
|
applicatif qui supprime les lignes échues, déclenché par un minuteur hors du chemin chaud —
|
|
@@ -735,7 +747,7 @@ c'est ce qui interdit de conclure « nouvelle mutation » hors d'une réservatio
|
|
|
735
747
|
en deux instructions au verdict non ambigu (`reserveIdempotencyKeyMysql()`, `queryKit.ts:152`).
|
|
736
748
|
|
|
737
749
|
**Le journal d'audit** pagine par un curseur **composite auto-portant** `<horodatage>:<id>` sur un ordre
|
|
738
|
-
total, plutôt que par un identifiant seul (`listPage()`, `DrizzleAuditStore.ts:
|
|
750
|
+
total, plutôt que par un identifiant seul (`listPage()`, `DrizzleAuditStore.ts:175`). Deux gains : plus
|
|
739
751
|
d'aller-retour pour résoudre le curseur, et une pagination qui ne rembobine pas à la première page si
|
|
740
752
|
l'événement de référence a été purgé entre deux appels.
|
|
741
753
|
|
|
@@ -834,7 +846,7 @@ faire lui-même).
|
|
|
834
846
|
Côté écrans : **Database**, **ORM (vue d'ensemble et par entité)** et **Stores** — ce dernier répond à
|
|
835
847
|
la question « où sont écrites mes données ? » pour chaque brique.
|
|
836
848
|
|
|
837
|
-
La sonde d'un connecteur s'adapte au dialecte (`probe()`, `DrizzleOrm.ts:
|
|
849
|
+
La sonde d'un connecteur s'adapte au dialecte (`probe()`, `DrizzleOrm.ts:1873`) :
|
|
838
850
|
|
|
839
851
|
- **SQLite** → `storage` : taille du fichier, mode de journal, pages libres (lus par `PRAGMA`) ;
|
|
840
852
|
- **PostgreSQL / MySQL** → `pool` : taille, connexions libres, empruntées, en attente — **compteurs en
|
|
@@ -851,7 +863,7 @@ l'emplacement **est** l'infra déclarée, déjà affichée ailleurs (`location`,
|
|
|
851
863
|
## ⚡ Performance & mémoire
|
|
852
864
|
|
|
853
865
|
**La sonde de requêtes ne coûte rien quand elle est éteinte.** Chaque exécution passe par un point de
|
|
854
|
-
mesure unique (`#prof()`, `DrizzleRepository.ts:
|
|
866
|
+
mesure unique (`#prof()`, `DrizzleRepository.ts:433`) qui alimente deux consommateurs — le profileur
|
|
855
867
|
par requête (barre de debug) et l'agrégat de flux. Les deux sont gardés par un drapeau : si aucun n'est
|
|
856
868
|
actif, la fonction rend le constructeur de requête tel quel, sans allocation. Le flux est **désactivé
|
|
857
869
|
en production** par défaut.
|
|
@@ -875,8 +887,7 @@ connexions. Les chiffres vivent dans la sortie du banc, pas ici — ils dépende
|
|
|
875
887
|
| -------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
876
888
|
| Une écriture dans `transaction()` n'est pas annulée | repository obtenu par `getRepository()` → passe par le **pool**, hors tx | `repo.withTransaction(tx)` — le seul moyen d'entrer dans la transaction |
|
|
877
889
|
| `NOT NULL constraint failed` sur une colonne pourtant « par défaut » | `.default()` SQL : le DDL dérivé ne l'émet pas | poser le défaut côté JS : `$defaultFn(() => …)` |
|
|
878
|
-
| Colonne ajoutée, table inchangée
|
|
879
|
-
| Index déclaré, absent en base | les index ne sortent que via `drizzle-kit` | migration `drizzle-kit` en production |
|
|
890
|
+
| Colonne **obligatoire** ajoutée, table inchangée | le rattrapage de développement n'ajoute que les colonnes facultatives | `nodefony orm:reset` (dev), ou `orm:generate` puis `orm:migrate` |
|
|
880
891
|
| Le démarrage échoue sur le connecteur | infra déclarée injoignable → `BootConfigurationError` (voulu) | corriger l'URL / démarrer la base / retirer le connecteur |
|
|
881
892
|
| `cannot start a transaction within a transaction` (SQLite) | deux `BEGIN` concurrents sur la connexion unique | rien à faire : la file d'attente interne les sérialise |
|
|
882
893
|
| Une entité fonctionne en SQLite, échoue en PostgreSQL | table Drizzle **figée** sur un dialecte, posée sur le connecteur `default` | fixer l'entité sur son propre connecteur SQLite, ou la porter au dialecte |
|
|
@@ -941,7 +952,7 @@ npm run test:load # charge, limites, mémoire
|
|
|
941
952
|
- 🧭 **L'abstraction au-dessus** : [`@nodefony/orm-core`](../../orm-core/docs/index.md) — contrats
|
|
942
953
|
`IOrm`/`IRepository`/`ITransaction`, `Criteria` et opérateurs riches, registres. À lire pour tout ce
|
|
943
954
|
qui est **portable** ; cette page-ci ne documente que le driver SQL.
|
|
944
|
-
- 🗄️ **Déployer un schéma** : [Migrations de schéma](migrations.md) — les
|
|
955
|
+
- 🗄️ **Déployer un schéma** : [Migrations de schéma](migrations.md) — les six commandes, le
|
|
945
956
|
rattrapage de développement, le travail d'orchestrateur et l'expansion/contraction.
|
|
946
957
|
- 📗 **Tutoriel** : [créer une entité pas à pas](../../orm-core/docs/tutorial-entity.md)
|
|
947
958
|
- 🧩 **L'autre driver** : [`@nodefony/mongoose`](../../mongoose/docs/index.md) — même contrat, MongoDB.
|
package/docs/migrations.md
CHANGED
|
@@ -426,7 +426,7 @@ Trois propriétés en découlent, et elles valent d'être nommées :
|
|
|
426
426
|
- **Rien n'est appliqué deux fois.** L'historique (`nodefony_migrations`, `types.ts:23`) est écrit
|
|
427
427
|
dans la même transaction que le DDL, là où le moteur le permet.
|
|
428
428
|
- **Un pod en retard ne reçoit pas de trafic.** En `none` comme en `migrate`, l'état du schéma est
|
|
429
|
-
publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:
|
|
429
|
+
publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:557`) : `/readyz`
|
|
430
430
|
répond 503, l'orchestrateur sort l'exemplaire du répartiteur de charge, et l'ancien continue de
|
|
431
431
|
servir. `/livez` n'est jamais touché — un schéma en retard n'est pas un processus malade, et le
|
|
432
432
|
redémarrer ne réparerait rien. La vérification est rejouée toutes les 15 secondes : dès que le
|