@onlineapps/conn-orch-validator 7.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2558 -2
- package/README.md +1038 -4
- package/docs/DESIGN.md +3 -1
- package/manifests/biz-service.manifest.json +658 -0
- package/manifests/library.manifest.json +324 -0
- package/package.json +12 -6
- package/src/CookbookTestRunner.js +408 -101
- package/src/CookbookTestUtils.js +7 -8
- package/src/ServiceReadinessValidator.js +10 -35
- package/src/ValidationOrchestrator.js +219 -71
- package/src/cli/biz-ci-gate.js +176 -33
- package/src/cli/oa-lint-scripts.js +221 -0
- package/src/cli/oa-sync-template.js +1020 -0
- package/src/cli/oa-validate.js +474 -0
- package/src/helpers/README.md +2 -1
- package/src/helpers/createServiceReadinessTests.js +60 -4
- package/src/index.js +33 -3
- package/src/lint/scripts/lintScripts.js +298 -0
- package/src/manifest/checks/composeRunnerBlock.js +222 -0
- package/src/manifest/checks/composeShape.js +165 -0
- package/src/manifest/checks/contractBridge.js +181 -0
- package/src/manifest/checks/discoveryOrphan.js +50 -0
- package/src/manifest/checks/docsLintBridge.js +553 -0
- package/src/manifest/checks/fileAbsent.js +35 -0
- package/src/manifest/checks/gitTracked.js +204 -0
- package/src/manifest/checks/index.js +111 -0
- package/src/manifest/checks/libraryContext.js +226 -0
- package/src/manifest/checks/libraryDocs.js +75 -0
- package/src/manifest/checks/libraryPackage.js +272 -0
- package/src/manifest/checks/librarySource.js +274 -0
- package/src/manifest/checks/libraryTests.js +121 -0
- package/src/manifest/checks/libraryWorkspace.js +293 -0
- package/src/manifest/checks/readmeRegion.js +135 -0
- package/src/manifest/checks/scriptHeaders.js +79 -0
- package/src/manifest/checks/serviceConfig.js +390 -0
- package/src/manifest/checks/serviceConnectors.js +81 -0
- package/src/manifest/checks/serviceDb.js +388 -0
- package/src/manifest/checks/serviceFiles.js +754 -0
- package/src/manifest/checks/serviceIdentityRows.js +351 -0
- package/src/manifest/checks/serviceRuntime.js +295 -0
- package/src/manifest/checks/serviceScripts.js +213 -0
- package/src/manifest/deployabilitySignal.js +121 -0
- package/src/manifest/discovery.js +386 -0
- package/src/manifest/loadManifest.js +62 -0
- package/src/manifest/manifestShape.js +446 -0
- package/src/manifest/report.js +245 -0
- package/src/manifest/runManifest.js +449 -0
- package/src/manifest/serviceIdentity.js +140 -0
- package/src/manifest/walk.js +74 -0
- package/src/manifest/workspaceRoot.js +242 -0
- package/src/mocks/MockMQClient.js +13 -30
- package/src/mocks/MockRegistry.js +4 -2
- package/src/mocks/MockStorage.js +4 -2
- package/src/sync/docsRegion.js +463 -0
- package/src/sync/generatedRegion.js +228 -0
- package/src/sync/readmeLocation.js +182 -0
- package/src/sync/readmePointer.js +477 -0
- package/src/sync/serviceTemplate.js +583 -0
- package/src/sync/sharedEnv.js +162 -0
- package/src/sync/uniformFiles.js +474 -0
- package/src/utils/bizCiGateContract.js +131 -7
- package/src/utils/connectorContract.js +97 -7
- package/src/utils/cookbookFormat.js +81 -40
- package/src/utils/deployContract.js +140 -9
- package/src/utils/envContract.js +57 -1
- package/src/utils/handlerRef.js +181 -0
- package/src/utils/installContract.js +287 -41
- package/src/utils/libCompat.js +29 -7
- package/src/utils/migrationOrder.js +163 -0
- package/src/utils/preValidation.js +20 -7
- package/src/utils/setupDatabase.js +194 -13
- package/src/utils/testCoverageContract.js +539 -0
- package/src/utils/testNamespace.js +247 -23
- package/src/utils/throwawaySchema.js +207 -0
- package/src/validators/ServiceStructureValidator.js +2 -1
- package/templates/business-service/.dockerignore +42 -0
- package/templates/business-service/.gitlab-ci.yml +290 -0
- package/templates/business-service/Dockerfile +27 -0
- package/templates/business-service/README.md +213 -0
- package/templates/business-service/config/biz-docs-lint.tree.json +10 -0
- package/templates/business-service/config/env-templates/__SERVICE_NAME__.env +4 -0
- package/templates/business-service/config/env-templates/shared.env +65 -0
- package/templates/business-service/config/service/config.json +14 -0
- package/templates/business-service/config/service/integration-contract.json +12 -0
- package/templates/business-service/config/service/operations.json +41 -0
- package/templates/business-service/docker-compose.production.yml +60 -0
- package/templates/business-service/docker-compose.yml +93 -0
- package/templates/business-service/docs/80-setup/INSTALL.md +101 -0
- package/templates/business-service/docs/80-setup/PLATFORM_MATRIX.md +65 -0
- package/templates/business-service/docs/80-setup/README.md +18 -0
- package/templates/business-service/docs/80-setup/VALIDATION.md +78 -0
- package/templates/business-service/docs/README.md +18 -0
- package/templates/business-service/gitignore +42 -0
- package/templates/business-service/index.js +10 -0
- package/templates/business-service/init.sh +54 -0
- package/templates/business-service/jest.config.js +6 -0
- package/templates/business-service/package.json.template +31 -0
- package/templates/business-service/scripts/verify-deploy-uniform.sh +180 -0
- package/templates/business-service/src/handlers/v3/echo.js +39 -0
- package/templates/business-service/tests/cookbooks/echo.json +36 -0
- package/templates/business-service/tests/unit/handler.test.js +78 -0
- package/src/WorkflowTestRunner.js +0 -402
|
@@ -74,15 +74,23 @@ function assertV3Operations(serviceRoot) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
+
* A run narrowed by `operationFilter` writes NO proof, and that is the whole
|
|
78
|
+
* reason the flag can exist. A validation proof is the claim that the service's
|
|
79
|
+
* cookbooks passed; a run of some of them cannot make that claim, and a proof
|
|
80
|
+
* written from one would let the registry trust a run that never happened —
|
|
81
|
+
* the same reason a FAILED run writes none.
|
|
82
|
+
*
|
|
77
83
|
* @param {object} params
|
|
78
84
|
* @param {string} params.serviceRoot service repository root
|
|
79
|
-
* @param {string} params.serviceUrl resolved by the caller from the service's own config
|
|
80
85
|
* @param {Function} params.RunnerClass CookbookTestRunner (injected so the module stays testable)
|
|
81
86
|
* @param {Function} params.ProofGeneratorClass ValidationProofGenerator
|
|
82
87
|
* @param {{log: Function, error: Function}} params.logger
|
|
83
|
-
* @
|
|
88
|
+
* @param {string} [params.operationFilter] JavaScript regular expression; only the
|
|
89
|
+
* cookbooks running a matching operation are executed
|
|
90
|
+
* @returns {Promise<{ok: boolean, results: object, failedSteps: Array, proof: object|null,
|
|
91
|
+
* proofPath: string|null, filteredBy: string|null}>}
|
|
84
92
|
*/
|
|
85
|
-
async function runPreValidation({ serviceRoot,
|
|
93
|
+
async function runPreValidation({ serviceRoot, RunnerClass, ProofGeneratorClass, logger, operationFilter }) {
|
|
86
94
|
const cookbooksDir = path.join(serviceRoot, COOKBOOKS_RELATIVE_DIR);
|
|
87
95
|
if (!fs.existsSync(cookbooksDir)) {
|
|
88
96
|
throw new Error(`[PreValidation] Missing cookbooks directory - ${cookbooksDir} is required. `
|
|
@@ -95,14 +103,15 @@ async function runPreValidation({ serviceRoot, serviceUrl, RunnerClass, ProofGen
|
|
|
95
103
|
|
|
96
104
|
const runner = new RunnerClass({
|
|
97
105
|
serviceName: packageJson.name,
|
|
98
|
-
serviceUrl,
|
|
99
106
|
servicePath: serviceRoot,
|
|
100
107
|
mockInfrastructure: true,
|
|
101
108
|
timeout: COOKBOOK_TIMEOUT_MS,
|
|
102
109
|
logger
|
|
103
110
|
});
|
|
104
111
|
|
|
105
|
-
const results = await runner.runCookbooks(cookbooksDir
|
|
112
|
+
const results = await runner.runCookbooks(cookbooksDir,
|
|
113
|
+
operationFilter === undefined ? {} : { operation: operationFilter });
|
|
114
|
+
const filteredBy = operationFilter === undefined ? null : operationFilter;
|
|
106
115
|
|
|
107
116
|
if (results.failed > 0) {
|
|
108
117
|
// No proof on a failed run. A proof is a claim that the cookbooks passed,
|
|
@@ -115,7 +124,11 @@ async function runPreValidation({ serviceRoot, serviceUrl, RunnerClass, ProofGen
|
|
|
115
124
|
validationErrors: step.validationErrors || []
|
|
116
125
|
}));
|
|
117
126
|
|
|
118
|
-
return { ok: false, results, failedSteps, proof: null, proofPath: null };
|
|
127
|
+
return { ok: false, results, failedSteps, proof: null, proofPath: null, filteredBy };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (filteredBy !== null) {
|
|
131
|
+
return { ok: true, results, failedSteps: [], proof: null, proofPath: null, filteredBy };
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
const proofGenerator = new ProofGeneratorClass({
|
|
@@ -131,7 +144,7 @@ async function runPreValidation({ serviceRoot, serviceUrl, RunnerClass, ProofGen
|
|
|
131
144
|
fs.mkdirSync(path.dirname(proofPath), { recursive: true });
|
|
132
145
|
fs.writeFileSync(proofPath, JSON.stringify(proof, null, 2));
|
|
133
146
|
|
|
134
|
-
return { ok: true, results, failedSteps: [], proof, proofPath };
|
|
147
|
+
return { ok: true, results, failedSteps: [], proof, proofPath, filteredBy };
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
module.exports = { runPreValidation, PROOF_RELATIVE_PATH, COOKBOOKS_RELATIVE_DIR };
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Two invariants, both from ADR 0006:
|
|
19
19
|
*
|
|
20
|
-
* §2 every .sql file in the declared directory is applied, in
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* §2 every .sql file in the declared directory is applied, in the order the
|
|
21
|
+
* operator runner applies them (`migrationOrder.js` — `sort -V`, the one
|
|
22
|
+
* definition of that order). No list lives in this file: the set grows with
|
|
23
|
+
* each schema change, and a list would drift until a missing table
|
|
24
|
+
* surfaced in an unrelated test.
|
|
23
25
|
*
|
|
24
|
-
* §3 foreign key checks stay ON. Deferring them would let a migration
|
|
26
|
+
* §3 the target is EMPTY and foreign key checks stay ON. Deferring them would let a migration
|
|
25
27
|
* reference a table a later file creates and still report success, which is
|
|
26
28
|
* the exact defect class building from empty exists to catch.
|
|
27
29
|
*
|
|
@@ -35,6 +37,8 @@ const fs = require('fs');
|
|
|
35
37
|
const path = require('path');
|
|
36
38
|
const { spawnSync } = require('child_process');
|
|
37
39
|
|
|
40
|
+
const { compareVersion } = require('./migrationOrder');
|
|
41
|
+
|
|
38
42
|
function requireValue(value, name) {
|
|
39
43
|
if (value === undefined || value === null || value === '') {
|
|
40
44
|
throw new Error(`[SetupDatabase] Missing ${name} - Expected a value. `
|
|
@@ -63,7 +67,9 @@ function resolveMigrationPlan(serviceRoot, database) {
|
|
|
63
67
|
const migrations = fs.readdirSync(dir, { withFileTypes: true })
|
|
64
68
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.sql'))
|
|
65
69
|
.map((entry) => entry.name)
|
|
66
|
-
|
|
70
|
+
// Version order, because that is what the operator runner uses to build the
|
|
71
|
+
// real installation — CI has to prove that artefact, not one of its own.
|
|
72
|
+
.sort(compareVersion)
|
|
67
73
|
.map((name) => path.join(dir, name));
|
|
68
74
|
|
|
69
75
|
if (migrations.length === 0) {
|
|
@@ -83,13 +89,26 @@ function resolveMigrationPlan(serviceRoot, database) {
|
|
|
83
89
|
return { dir, migrations, seeds };
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Run one statement or file through the CLI client. Injectable for tests.
|
|
94
|
+
*
|
|
95
|
+
* `capture: true` asks for the client's stdout as well, in the bare shape a
|
|
96
|
+
* caller can read — one value per line, no column names and no box drawing.
|
|
97
|
+
* Only the emptiness probe uses it; a migration produces no output to read.
|
|
98
|
+
*/
|
|
99
|
+
function defaultExec({ connection, schema, sql, file, capture = false }) {
|
|
88
100
|
const client = ['mariadb', 'mysql'].find((bin) => !spawnSync(bin, ['--version'], { encoding: 'utf8' }).error);
|
|
89
101
|
if (!client) {
|
|
90
102
|
return {
|
|
91
103
|
status: 1,
|
|
92
|
-
|
|
104
|
+
// The reason is unconditional, and it says so: every statement of this
|
|
105
|
+
// build goes through the CLI, the first CREATE DATABASE included, and
|
|
106
|
+
// there is no driver path here. It used to read "required because
|
|
107
|
+
// migrations use DELIMITER", which BIZ-hello met on 2026-09-14 with no
|
|
108
|
+
// DELIMITER anywhere in its migrations — a service without triggers read
|
|
109
|
+
// it as "this does not apply to me" (`architecture-principles.md` §5).
|
|
110
|
+
stderr: '[SetupDatabase] No mysql/mariadb client on PATH - every statement of this build runs through '
|
|
111
|
+
+ 'the CLI, the first CREATE DATABASE included, and there is no driver path. '
|
|
93
112
|
+ 'Fix: install mariadb-client in the CI job image.'
|
|
94
113
|
};
|
|
95
114
|
}
|
|
@@ -99,6 +118,7 @@ function defaultExec({ connection, schema, sql, file }) {
|
|
|
99
118
|
// MariaDB Connector/C 3.x requires TLS by default and the CI database is a
|
|
100
119
|
// sidecar with none; the connection never leaves the job's private network.
|
|
101
120
|
if (connection.requireTls !== true) args.push('--skip-ssl');
|
|
121
|
+
if (capture === true) args.push('--batch', '--skip-column-names');
|
|
102
122
|
if (connection.password) args.push(`-p${connection.password}`);
|
|
103
123
|
if (schema) args.push(schema);
|
|
104
124
|
|
|
@@ -109,11 +129,160 @@ function defaultExec({ connection, schema, sql, file }) {
|
|
|
109
129
|
|
|
110
130
|
return {
|
|
111
131
|
status: result.error ? 1 : result.status,
|
|
132
|
+
stdout: result.error ? '' : (result.stdout || ''),
|
|
112
133
|
stderr: result.error ? result.error.message
|
|
113
134
|
: (result.stderr || '').split('\n').filter((l) => !l.includes('Using a password')).join('\n').trim()
|
|
114
135
|
};
|
|
115
136
|
}
|
|
116
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Refuse a target that is not a throwaway.
|
|
140
|
+
*
|
|
141
|
+
* `database.schema` is the REAL name — `oagen_emailer`, the same string on the
|
|
142
|
+
* dev server and in production — so the name distinguishes nothing, and neither
|
|
143
|
+
* does "does the schema exist": every biz `.gitlab-ci.yml` has the sidecar
|
|
144
|
+
* create it empty through `MARIADB_DATABASE` before the job starts.
|
|
145
|
+
*
|
|
146
|
+
* What does distinguish them is the premise this whole build rests on: the
|
|
147
|
+
* migration set is proved by applying it to an EMPTY schema (ADR 0006 §3). A
|
|
148
|
+
* target that already holds tables breaks that premise twice over — the run
|
|
149
|
+
* proves nothing, because the tables it would create are already there, and it
|
|
150
|
+
* writes into somebody's data, because a declared seed carries no
|
|
151
|
+
* `IF NOT EXISTS`. Both halves are the same answer: stop.
|
|
152
|
+
*
|
|
153
|
+
* Reported as UNCERTAIN by BIZ-emailer on 2026-09-04 — "we found nothing that
|
|
154
|
+
* blocks such a run outside CI" — and there was nothing.
|
|
155
|
+
*/
|
|
156
|
+
function assertTargetIsEmpty({ exec, connection, schema }) {
|
|
157
|
+
const probe = exec({
|
|
158
|
+
connection,
|
|
159
|
+
capture: true,
|
|
160
|
+
sql: `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${schema}';`
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (probe.status !== 0) {
|
|
164
|
+
throw new Error(`[SetupDatabase] Could not ask ${connection.host}:${connection.port} what ${schema} holds:\n`
|
|
165
|
+
+ `${probe.stderr}\n`
|
|
166
|
+
+ ' Fix: the build refuses to touch a schema it cannot inspect — give this account SELECT on '
|
|
167
|
+
+ 'information_schema, or point DB_HOST at the CI database.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const count = Number.parseInt(String(probe.stdout ?? '').trim(), 10);
|
|
171
|
+
if (!Number.isFinite(count)) {
|
|
172
|
+
throw new Error(`[SetupDatabase] The table count for ${schema} came back unreadable: `
|
|
173
|
+
+ `"${String(probe.stdout ?? '').trim()}"\n`
|
|
174
|
+
+ ' Fix: the executor must return the client stdout for a capture call; without a count '
|
|
175
|
+
+ 'this build cannot tell a throwaway schema from a live one.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (count > 0) {
|
|
179
|
+
throw new Error(`[SetupDatabase] Schema ${schema} on ${connection.host}:${connection.port} already holds `
|
|
180
|
+
+ `${count} table(s) - refusing to build.\n`
|
|
181
|
+
+ ' The migration set is proved by applying it to an EMPTY schema (ADR 0006 §3); against a '
|
|
182
|
+
+ 'populated one it proves nothing, and a seed would write into whatever is already there.\n'
|
|
183
|
+
+ ' Fix: point DB_HOST at the throwaway database of the CI job (the sidecar creates it empty), '
|
|
184
|
+
+ `or drop ${schema} first if this really is a disposable instance. Never aim this at a dev or `
|
|
185
|
+
+ 'production server.');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The collation the schema must carry, or a refusal.
|
|
191
|
+
*
|
|
192
|
+
* Owner decision 2026-09-14 (`db-collation-declaration` 001): the collation is
|
|
193
|
+
* the service's own declaration, `database.collation` in the integration
|
|
194
|
+
* contract, and the same key the production runbook reads. There is no default
|
|
195
|
+
* and no server fallback — a DB-owning service that declares none stops the
|
|
196
|
+
* build (`architecture-principles.md` §3).
|
|
197
|
+
*
|
|
198
|
+
* Whether the VALUE is a collation name is the contract validator's rule
|
|
199
|
+
* (`bizCiGateContract.js`, which every gate reads the contract through), and
|
|
200
|
+
* whether a repository DECLARED one is uniform row `D-DB-COLLATION`. What
|
|
201
|
+
* belongs here is neither of those: this function cannot build without the
|
|
202
|
+
* value, so it refuses at its own entry (§4), the way `DB_HOST` is refused.
|
|
203
|
+
*
|
|
204
|
+
* @param {{collation?: string}} database the contract declaration
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function requireCollation(database) {
|
|
208
|
+
const declared = database.collation;
|
|
209
|
+
if (typeof declared === 'string' && declared.trim() !== '') return declared.trim();
|
|
210
|
+
|
|
211
|
+
throw new Error('[SetupDatabase] Missing database.collation - the schema is created with the collation the '
|
|
212
|
+
+ 'service declares, and there is no default.\n'
|
|
213
|
+
+ ' Fix: add "collation" to the "database" block of config/service/integration-contract.json '
|
|
214
|
+
+ '(e.g. "utf8mb4_bin" — the collation this service\'s tables already use); '
|
|
215
|
+
+ 'api/docs/biz/70-contracts/database-contract.md §1 owns the key, '
|
|
216
|
+
+ 'api/docs/governance/confirmations/db-collation-declaration.md 001 the decision.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What the server says the schema was created with, or null when there is none.
|
|
221
|
+
*
|
|
222
|
+
* This probe is the reason `CREATE DATABASE IF NOT EXISTS` is not enough, and
|
|
223
|
+
* the reason is measured rather than assumed (BIZ-DOCS, 2026-09-14, throwaway
|
|
224
|
+
* `mariadb:10.5.29`): over an existing database, `IF NOT EXISTS … COLLATE`
|
|
225
|
+
* changes nothing and reports success. Six biz pipelines have the sidecar
|
|
226
|
+
* create the schema from `MARIADB_DATABASE` before the job starts, so in the
|
|
227
|
+
* environment this gate actually runs in the database is already there — with
|
|
228
|
+
* the image default `utf8mb4_general_ci`. Creating "if not exists" would
|
|
229
|
+
* therefore report a schema built to the declaration while the server holds
|
|
230
|
+
* another one, which is the false guarantee `automation-gates.md` §5 names.
|
|
231
|
+
*
|
|
232
|
+
* @returns {string|null}
|
|
233
|
+
*/
|
|
234
|
+
function existingCollation({ exec, connection, schema }) {
|
|
235
|
+
const probe = exec({
|
|
236
|
+
connection,
|
|
237
|
+
capture: true,
|
|
238
|
+
sql: 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA '
|
|
239
|
+
+ `WHERE SCHEMA_NAME = '${schema}';`
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (probe.status !== 0) {
|
|
243
|
+
throw new Error(`[SetupDatabase] Could not ask ${connection.host}:${connection.port} what collation `
|
|
244
|
+
+ `${schema} was created with:\n${probe.stderr}\n`
|
|
245
|
+
+ ' Fix: the build refuses to create or reuse a schema whose collation it cannot read — give this '
|
|
246
|
+
+ 'account SELECT on information_schema, or point DB_HOST at the CI database.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const answer = String(probe.stdout ?? '').trim();
|
|
250
|
+
return answer === '' ? null : answer;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create the schema with the declared collation, or refuse a schema that
|
|
255
|
+
* already carries another one.
|
|
256
|
+
*
|
|
257
|
+
* Never `IF NOT EXISTS`: see `existingCollation` above for what that silence
|
|
258
|
+
* costs.
|
|
259
|
+
*/
|
|
260
|
+
function createDatabaseSql(schema, collation) {
|
|
261
|
+
return `CREATE DATABASE \`${schema}\` COLLATE ${collation};`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createSchema({ exec, connection, schema, collation }) {
|
|
265
|
+
const present = existingCollation({ exec, connection, schema });
|
|
266
|
+
|
|
267
|
+
if (present === collation) return;
|
|
268
|
+
|
|
269
|
+
if (present !== null) {
|
|
270
|
+
throw new Error(`[SetupDatabase] Schema ${schema} on ${connection.host}:${connection.port} was created `
|
|
271
|
+
+ `with collation ${present}, and this service declares ${collation} - refusing to build.\n`
|
|
272
|
+
+ ' A CREATE DATABASE IF NOT EXISTS over an existing database changes nothing and reports '
|
|
273
|
+
+ 'success, so the run would prove a schema it never built.\n'
|
|
274
|
+
+ ` Fix: create the database with the declared collation — in CI give the database service `
|
|
275
|
+
+ `MARIADB_COLLATION=${collation} beside MARIADB_DATABASE (or drop MARIADB_DATABASE and let this `
|
|
276
|
+
+ `gate create it); on an existing instance, ALTER DATABASE \`${schema}\` COLLATE ${collation}. `
|
|
277
|
+
+ 'The key itself is api/docs/biz/70-contracts/database-contract.md §1.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const created = exec({ connection, sql: createDatabaseSql(schema, collation) });
|
|
281
|
+
if (created.status !== 0) {
|
|
282
|
+
throw new Error(`[SetupDatabase] Could not create schema ${schema}:\n${created.stderr}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
117
286
|
/**
|
|
118
287
|
* Build the schema from the declared migrations.
|
|
119
288
|
*
|
|
@@ -125,12 +294,11 @@ function buildSchema({ serviceRoot, database, connection, exec = defaultExec })
|
|
|
125
294
|
const port = connection.port ?? 3306;
|
|
126
295
|
|
|
127
296
|
const conn = { ...connection, port };
|
|
297
|
+
const collation = requireCollation(database);
|
|
128
298
|
const plan = resolveMigrationPlan(serviceRoot, database);
|
|
129
299
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new Error(`[SetupDatabase] Could not create schema ${database.schema}:\n${created.stderr}`);
|
|
133
|
-
}
|
|
300
|
+
assertTargetIsEmpty({ exec, connection: conn, schema: database.schema });
|
|
301
|
+
createSchema({ exec, connection: conn, schema: database.schema, collation });
|
|
134
302
|
|
|
135
303
|
const apply = (file, kind) => {
|
|
136
304
|
const result = exec({ connection: conn, schema: database.schema, file });
|
|
@@ -151,4 +319,17 @@ function buildSchema({ serviceRoot, database, connection, exec = defaultExec })
|
|
|
151
319
|
};
|
|
152
320
|
}
|
|
153
321
|
|
|
154
|
-
|
|
322
|
+
// `requireCollation`, `createDatabaseSql` and `defaultExec` are exported for
|
|
323
|
+
// `throwawaySchema.js`, the build an integration test uses. The two builds
|
|
324
|
+
// differ in exactly one decision — the CI gate refuses a target it did not
|
|
325
|
+
// create, the test one drops and recreates its own — and in nothing else: the
|
|
326
|
+
// migration set and its order, the collation requirement, the CREATE statement
|
|
327
|
+
// and the client invocation have one definition each, here
|
|
328
|
+
// (`change-discipline.md` § One rail per concern).
|
|
329
|
+
module.exports = {
|
|
330
|
+
resolveMigrationPlan,
|
|
331
|
+
buildSchema,
|
|
332
|
+
requireCollation,
|
|
333
|
+
createDatabaseSql,
|
|
334
|
+
defaultExec
|
|
335
|
+
};
|