@snolla/data 0.0.0-stage → 1.0.7
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/dist/actor.d.ts +28 -0
- package/dist/actor.js +63 -0
- package/dist/audit.d.ts +36 -0
- package/dist/audit.js +198 -0
- package/dist/dataProvider.d.ts +41 -0
- package/dist/dataProvider.js +148 -0
- package/dist/dataSource.d.ts +4 -0
- package/dist/dataSource.js +65 -0
- package/dist/dbProvider.d.ts +1261 -0
- package/dist/dbProvider.js +1898 -0
- package/dist/freeze.d.ts +29 -0
- package/dist/freeze.js +69 -0
- package/dist/guard.d.ts +53 -0
- package/dist/guard.js +397 -0
- package/dist/insert.d.ts +9 -0
- package/dist/insert.js +44 -0
- package/dist/migrations/siteid.d.mts +738 -0
- package/dist/migrations/siteid.mjs +512 -0
- package/dist/publish.d.ts +1 -0
- package/dist/publish.js +1 -0
- package/dist/schema/dsl.d.ts +64 -0
- package/dist/schema/dsl.js +28 -0
- package/dist/schema/generate.d.mts +14 -0
- package/dist/schema/generate.mjs +282 -0
- package/dist/schema/generated/ddl.d.ts +7 -0
- package/dist/schema/generated/ddl.js +11 -0
- package/dist/schema/generated/kysely-types.d.ts +1988 -0
- package/dist/schema/generated/kysely-types.js +5 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/provision.d.mts +11 -0
- package/dist/schema/provision.mjs +111 -0
- package/dist/schema/tables.d.ts +2 -0
- package/dist/schema/tables.js +3301 -0
- package/dist/scripts/canon-audit.d.mts +25 -0
- package/dist/scripts/canon-audit.mjs +213 -0
- package/dist/scripts/canon-smoke.d.mts +1 -0
- package/dist/scripts/canon-smoke.mjs +44 -0
- package/dist/scripts/dialect-ddl-stand.d.mts +1 -0
- package/dist/scripts/dialect-ddl-stand.mjs +136 -0
- package/dist/scripts/owner-spec-check.d.mts +1 -0
- package/dist/scripts/owner-spec-check.mjs +166 -0
- package/dist/scripts/owners/owner-resolve-spec.json +540 -0
- package/dist/scripts/owners-audit.d.mts +60 -0
- package/dist/scripts/owners-audit.mjs +443 -0
- package/dist/scripts/owners-fixture.d.mts +20 -0
- package/dist/scripts/owners-fixture.mjs +192 -0
- package/dist/scripts/schema-lengths-audit.d.mts +22 -0
- package/dist/scripts/schema-lengths-audit.mjs +146 -0
- package/dist/scripts/siteid-migrate.d.mts +7 -0
- package/dist/scripts/siteid-migrate.mjs +260 -0
- package/dist/scripts/tenant-invariant-audit.d.mts +1 -0
- package/dist/scripts/tenant-invariant-audit.mjs +34 -0
- package/dist/singleMode.d.ts +5 -0
- package/dist/singleMode.js +23 -0
- package/dist/sqliteDialect.d.ts +6 -0
- package/dist/sqliteDialect.js +92 -0
- package/dist/templateDefs.d.ts +38 -0
- package/dist/templateDefs.js +163 -0
- package/dist/test/fixtures/broken-driver/index.d.ts +3 -0
- package/dist/test/fixtures/broken-driver/index.js +5 -0
- package/dist/test/support/dialect-stand.d.ts +37 -0
- package/dist/test/support/dialect-stand.js +162 -0
- package/dist/test/support/render-parity.d.ts +44 -0
- package/dist/test/support/render-parity.js +262 -0
- package/package.json +50 -5
- package/README.md +0 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// schema-as-file — portable, file-based TEMPLATE DEFINITIONS (snolla-dev-workflow Фаза 0).
|
|
2
|
+
//
|
|
3
|
+
// A "template-def" (the legacy MoreThenCms ContentTools concept) is the definition of an
|
|
4
|
+
// entity/page template: its field list + each field's type. In prod these live DB-only —
|
|
5
|
+
// a row in a `*Template` model whose `Content` column holds a JSON STRING: an array of
|
|
6
|
+
// field-defs `{ name, fieldTemplate, value, valueType, options, partTemplate, ... }`.
|
|
7
|
+
// `@snolla/core`'s `fillViewModel` and `@snolla/content-schema-gen` both already consume
|
|
8
|
+
// exactly that array (via `JSON.parse(template.content)`).
|
|
9
|
+
//
|
|
10
|
+
// schema-as-file makes the local template file CANONICAL (design §"Local template is
|
|
11
|
+
// canonical"): the same field-def vocabulary, authored as a readable/diffable JSON file
|
|
12
|
+
// in the repo, from which mocks/defaults/lint/intellisense/type-gen derive. We deliberately
|
|
13
|
+
// REUSE the existing field-def shape (a derivative of the content-schema-gen parse) rather than
|
|
14
|
+
// invent a new JSON-schema-like DSL — one parser feeds both render and codegen, zero
|
|
15
|
+
// translation drift. Prior art: Strapi `schema.json`-in-repo (adopt the PATTERN — one
|
|
16
|
+
// canonical schema file per template — but keep snolla's native `fieldTemplate` vocabulary,
|
|
17
|
+
// which is already proven in prod, instead of Strapi's attribute vocabulary).
|
|
18
|
+
//
|
|
19
|
+
// This module is PURE (no Sequelize/config) and lives at the `@snolla/data` floor so both
|
|
20
|
+
// the engine (`@snolla/core`) and the codegen tool (`@snolla/content-schema-gen`) can import
|
|
21
|
+
// it without pulling the DB barrel. Import the subpath directly:
|
|
22
|
+
// import { parseTemplateDef, loadTemplateDefFile } from '@snolla/data/templateDefs.js';
|
|
23
|
+
//
|
|
24
|
+
// FILE SHAPE (readable — `fields` is a real array, not a JSON-string-in-JSON):
|
|
25
|
+
// {
|
|
26
|
+
// "kind": "pageTemplate", // which template model this def is for
|
|
27
|
+
// "id": "…uuid…", // optional — maps to the DB row on sync; absent = greenfield
|
|
28
|
+
// "alias": "landing", // required — the discriminator used everywhere (template.alias)
|
|
29
|
+
// "name": "Landing Page", // optional — defaults to alias
|
|
30
|
+
// "customDefaultLayout": "landing", // optional — liquid layout name
|
|
31
|
+
// "layouts": { … }, // optional — named-layouts blob (object or string)
|
|
32
|
+
// "fields": [ // the field-defs — same vocabulary as DB Content JSON
|
|
33
|
+
// { "name": "headline", "fieldTemplate": "textEditorTemplate", "value": "Welcome" },
|
|
34
|
+
// { "name": "parts", "fieldTemplate": "partEditorTemplate", "partTemplate": "hero" }
|
|
35
|
+
// ]
|
|
36
|
+
// }
|
|
37
|
+
//
|
|
38
|
+
// `parseTemplateDef` NORMALIZES this to the DB-loaded shape (a drop-in for a Sequelize
|
|
39
|
+
// template instance): `{ id, kind, alias, name, description, customDefaultLayout, layouts,
|
|
40
|
+
// content }` where `content` is `JSON.stringify(fields)` — so downstream `JSON.parse(content)`
|
|
41
|
+
// yields the original `fields` verbatim (the round-trip invariant). `kind` is additive
|
|
42
|
+
// routing metadata the DB row lacks (the table implies it); the DB scoping FK (siteId/ownerId)
|
|
43
|
+
// is likewise absent — a file in the repo is scoped by its site/location, and render-time
|
|
44
|
+
// site context is passed to fillViewModel separately.
|
|
45
|
+
import { readFileSync } from 'fs';
|
|
46
|
+
// Template model kinds (recon §1 — every `*Template` model in packages/data/sequelize/snolla).
|
|
47
|
+
// A file-def declares which one it is so a loader/codegen routes it to the right model/group.
|
|
48
|
+
export const TEMPLATE_KINDS = new Set([
|
|
49
|
+
'pageTemplate', 'pagePartTemplate',
|
|
50
|
+
'blogPostTemplate', 'blogPostPartTemplate',
|
|
51
|
+
'storeProductTemplate', 'storeProductPartTemplate', 'storeProductVariantTemplate', 'storePartTemplate',
|
|
52
|
+
'catalogProductTemplate', 'catalogProductPartTemplate',
|
|
53
|
+
'reviewTemplate',
|
|
54
|
+
'tableTemplate', 'tableRecordPartTemplate',
|
|
55
|
+
]);
|
|
56
|
+
// The field-type vocabulary — every `fieldTemplate` value the render engine handles.
|
|
57
|
+
// GROUND TRUTH is the switch in `@snolla/core` viewModels/index.js `fillViewModel`
|
|
58
|
+
// (recon §3); this set MUST stay in sync with it (the Ф2 linter should reconcile the two
|
|
59
|
+
// mechanically). Validating file-defs against this set is the seed of "schema-as-file is
|
|
60
|
+
// canonical → lint derives from it": an unknown/typo'd type is caught at load, not silently
|
|
61
|
+
// rendered empty.
|
|
62
|
+
export const FIELD_TEMPLATE_TYPES = new Set([
|
|
63
|
+
// string / text / html (pass-through)
|
|
64
|
+
'colorEditorTemplate', 'markItUpHtmlEditorTemplate', 'textAreaEditorTemplate',
|
|
65
|
+
'textEditorTemplate', 'telEditorTemplate', 'emailEditorTemplate', 'urlEditorTemplate',
|
|
66
|
+
'tinymceEditorTemplate',
|
|
67
|
+
// markup-rendered
|
|
68
|
+
'markItUpMarkdownEditorTemplate', 'markItUpTextileEditorTemplate',
|
|
69
|
+
// literals (value from template only)
|
|
70
|
+
'hiddenFieldTemplate', 'htmlLiteralTemplate', 'markdownLiteralTemplate', 'textileLiteralTemplate',
|
|
71
|
+
// structured
|
|
72
|
+
'jsonEditorTemplate',
|
|
73
|
+
// numeric
|
|
74
|
+
'numericEditorTemplate', 'integerEditorTemplate',
|
|
75
|
+
// date / time
|
|
76
|
+
'dateTimeEditorTemplate', 'dateEditorTemplate', 'timeEditorTemplate',
|
|
77
|
+
// boolean / selection
|
|
78
|
+
'checkboxEditorTemplate', 'selectListEditorTemplate', 'radioButtonsEditorTemplate', 'checkboxesEditorTemplate',
|
|
79
|
+
// media (gallery-backed)
|
|
80
|
+
'galleryEditorTemplate', 'imageEditorTemplate',
|
|
81
|
+
// composition / relational
|
|
82
|
+
'partEditorTemplate', 'linkedItemEditorTemplate',
|
|
83
|
+
// no-op / TODO (accepted by the engine as inert)
|
|
84
|
+
'feedSelectListEditorTemplate', 'fileUploadTemplate',
|
|
85
|
+
]);
|
|
86
|
+
function validateFields(fields, ctx) {
|
|
87
|
+
if (!Array.isArray(fields)) {
|
|
88
|
+
throw new Error(`template-def ${ctx}: "fields" must be an array`);
|
|
89
|
+
}
|
|
90
|
+
fields.forEach((f, i) => {
|
|
91
|
+
if (!f || typeof f !== 'object' || Array.isArray(f)) {
|
|
92
|
+
throw new Error(`template-def ${ctx}: field[${i}] must be an object`);
|
|
93
|
+
}
|
|
94
|
+
if (!f.name || typeof f.name !== 'string') {
|
|
95
|
+
throw new Error(`template-def ${ctx}: field[${i}] is missing a string "name"`);
|
|
96
|
+
}
|
|
97
|
+
if (!f.fieldTemplate || !FIELD_TEMPLATE_TYPES.has(f.fieldTemplate)) {
|
|
98
|
+
throw new Error(`template-def ${ctx}: field "${f.name}" has unknown fieldTemplate ${JSON.stringify(f.fieldTemplate)}`);
|
|
99
|
+
}
|
|
100
|
+
if (f.fieldTemplate === 'partEditorTemplate' && (!f.partTemplate || typeof f.partTemplate !== 'string')) {
|
|
101
|
+
throw new Error(`template-def ${ctx}: part field "${f.name}" requires a string "partTemplate" (the referenced part-template alias)`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate + normalize a schema-as-file template-def object into the DB-loaded shape.
|
|
107
|
+
* Pure. Throws on structural / vocabulary errors (the load-time lint seam).
|
|
108
|
+
*
|
|
109
|
+
* @param {object} def parsed file object (see FILE SHAPE above)
|
|
110
|
+
* @returns {{id, kind, alias, name, description, customDefaultLayout, layouts, content}}
|
|
111
|
+
* `content` is `JSON.stringify(def.fields)` — a drop-in for a Sequelize template row.
|
|
112
|
+
*/
|
|
113
|
+
export function parseTemplateDef(def) {
|
|
114
|
+
if (!def || typeof def !== 'object' || Array.isArray(def)) {
|
|
115
|
+
throw new Error('template-def must be an object');
|
|
116
|
+
}
|
|
117
|
+
const { kind, alias } = def;
|
|
118
|
+
if (!kind || !TEMPLATE_KINDS.has(kind)) {
|
|
119
|
+
throw new Error(`template-def: unknown kind ${JSON.stringify(kind)} — expected one of: ${[...TEMPLATE_KINDS].join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
if (!alias || typeof alias !== 'string') {
|
|
122
|
+
throw new Error(`template-def (${kind}): "alias" is required (a string)`);
|
|
123
|
+
}
|
|
124
|
+
const fields = def.fields == null ? [] : def.fields;
|
|
125
|
+
validateFields(fields, `${kind}:${alias}`);
|
|
126
|
+
return {
|
|
127
|
+
id: def.id,
|
|
128
|
+
kind,
|
|
129
|
+
alias,
|
|
130
|
+
name: def.name == null ? alias : def.name,
|
|
131
|
+
description: def.description == null ? null : def.description,
|
|
132
|
+
customDefaultLayout: def.customDefaultLayout == null ? null : def.customDefaultLayout,
|
|
133
|
+
layouts: def.layouts == null
|
|
134
|
+
? null
|
|
135
|
+
: (typeof def.layouts === 'string' ? def.layouts : JSON.stringify(def.layouts)),
|
|
136
|
+
content: JSON.stringify(fields),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Read + parse a template-def JSON file from disk into the normalized DB-loaded shape.
|
|
141
|
+
* The thin IO wrapper over `parseTemplateDef` (the file-loader the engine plugs in alongside
|
|
142
|
+
* its DB loader).
|
|
143
|
+
*
|
|
144
|
+
* @param {string} filePath absolute or cwd-relative path to a template-def `.json` file
|
|
145
|
+
* @returns {{id, kind, alias, name, description, customDefaultLayout, layouts, content}}
|
|
146
|
+
*/
|
|
147
|
+
export function loadTemplateDefFile(filePath) {
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = readFileSync(filePath, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
throw new Error(`template-def file ${filePath}: cannot read — ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
let obj;
|
|
156
|
+
try {
|
|
157
|
+
obj = JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
throw new Error(`template-def file ${filePath}: invalid JSON — ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
return parseTemplateDef(obj);
|
|
163
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function fillCanonRow(table: any, values?: {}): {};
|
|
2
|
+
export function dialectDdl(dialect: any, tables: any, { constraints }?: {
|
|
3
|
+
constraints?: boolean;
|
|
4
|
+
}): string;
|
|
5
|
+
export function sqliteStand({ ddl }: {
|
|
6
|
+
ddl: any;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
db: Kysely<any>;
|
|
9
|
+
query: (statement: any) => Promise<Record<string, import("node:sqlite").SQLOutputValue>[]>;
|
|
10
|
+
cleanup: () => Promise<void>;
|
|
11
|
+
}>;
|
|
12
|
+
export function postgresStand(url: any, { ddl, database }: {
|
|
13
|
+
ddl: any;
|
|
14
|
+
database?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
db: Kysely<any>;
|
|
17
|
+
url: string;
|
|
18
|
+
cleanup: () => Promise<void>;
|
|
19
|
+
}>;
|
|
20
|
+
export function mariadbStand(url: any, { ddl, database }: {
|
|
21
|
+
ddl: any;
|
|
22
|
+
database?: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
db: Kysely<any>;
|
|
25
|
+
url: any;
|
|
26
|
+
cleanup: () => Promise<void>;
|
|
27
|
+
}>;
|
|
28
|
+
export function stringColumnType(dialect: any, length: any): any;
|
|
29
|
+
export function mssqlStand(dsn: any, { ddl, database }: {
|
|
30
|
+
ddl: any;
|
|
31
|
+
database?: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
db: Kysely<any>;
|
|
34
|
+
url: any;
|
|
35
|
+
cleanup: () => Promise<void>;
|
|
36
|
+
}>;
|
|
37
|
+
import { Kysely } from 'kysely';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Shared stand harness for the dialect tests (task:3822, task:3821).
|
|
2
|
+
//
|
|
3
|
+
// Not a test file: the plumbing every dialect test needs — the node:sqlite Kysely
|
|
4
|
+
// dialect, a fresh database per run, and the generated DDL narrowed to a set of
|
|
5
|
+
// tables. The DDL always comes from the generated artifact (`renderDdlSql`), never
|
|
6
|
+
// from a hand-written schema, so a dialect test exercises what the generator emits.
|
|
7
|
+
//
|
|
8
|
+
// const stand = await sqliteStand({ ddl: dialectDdl('sqlite', ['Pages']) });
|
|
9
|
+
// try { ...stand.db... } finally { await stand.cleanup(); }
|
|
10
|
+
import { Kysely, MysqlDialect, PostgresDialect } from 'kysely';
|
|
11
|
+
import { canonicalSchema } from "../../schema/index.js";
|
|
12
|
+
import { mapType, renderDdlSql } from '../../schema/generate.mjs';
|
|
13
|
+
import { nodeSqliteDialect, openSqlite } from '../../sqliteDialect.js';
|
|
14
|
+
/**
|
|
15
|
+
* Every NOT NULL column the canon declares, filled with a type-correct value so a
|
|
16
|
+
* test can insert a row without spelling out the whole table. `SiteId` is never
|
|
17
|
+
* filled: the guard owns the tenant key (R1), and a caller that wants it passes it
|
|
18
|
+
* explicitly in `values`.
|
|
19
|
+
*/
|
|
20
|
+
export const fillCanonRow = (table, values = {}) => {
|
|
21
|
+
const spec = canonicalSchema.tables.find((candidate) => candidate.name === table);
|
|
22
|
+
if (!spec)
|
|
23
|
+
throw new Error(`fillCanonRow: "${table}" is not a canon table`);
|
|
24
|
+
const row = { ...values };
|
|
25
|
+
for (const [name, column] of Object.entries(spec.columns)) {
|
|
26
|
+
if (name === 'SiteId' || column.nullable === true || column.default !== undefined || name in row)
|
|
27
|
+
continue;
|
|
28
|
+
row[name] =
|
|
29
|
+
column.type === 'bool' ? false
|
|
30
|
+
: column.type === 'int' || column.type === 'bigint' ? 0
|
|
31
|
+
: column.type === 'decimal' ? '0'
|
|
32
|
+
: column.type === 'datetime' || column.type === 'date' ? '2026-01-02 03:04:05'
|
|
33
|
+
: column.type === 'uuid' ? '99999999-9999-9999-9999-999999999999'
|
|
34
|
+
: 'x';
|
|
35
|
+
}
|
|
36
|
+
return row;
|
|
37
|
+
};
|
|
38
|
+
/** Generated DDL, narrowed to the tables a scenario touches (order preserved). */
|
|
39
|
+
export const dialectDdl = (dialect, tables, { constraints = true } = {}) => {
|
|
40
|
+
const subjects = new Set(tables);
|
|
41
|
+
return renderDdlSql(dialect)
|
|
42
|
+
.split(/;\r?\n/)
|
|
43
|
+
.map((statement) => statement.trim())
|
|
44
|
+
.filter((statement) => statement && subjects.has(subjectTable(statement)))
|
|
45
|
+
// a narrowed stand may create one table whose parents are absent: the constraint
|
|
46
|
+
// would reference a table nobody built (SQL Server refuses immediately)
|
|
47
|
+
.filter((statement) => constraints || subjectTable(statement) === null || !/^alter\s+table/i.test(codeOf(statement)))
|
|
48
|
+
.map((statement) => `${statement};`)
|
|
49
|
+
.join('\n');
|
|
50
|
+
};
|
|
51
|
+
/** The statement with the generator's leading comments stripped. */
|
|
52
|
+
const codeOf = (statement) => statement.replace(/^(?:\s*--[^\n]*\n)+/, '').trim();
|
|
53
|
+
/** The table a generated statement acts on — `create table`, `alter table`, `create index`. */
|
|
54
|
+
const subjectTable = (statement) => {
|
|
55
|
+
// the first chunk carries the generator's header comment; soft-fk comments ride
|
|
56
|
+
// ahead of the statement they describe
|
|
57
|
+
const code = statement.replace(/^(?:\s*--[^\n]*\n)+/, '').trim();
|
|
58
|
+
// `"x"` / `` `x` `` / `[x]` — the quoting differs per dialect, the shape does not
|
|
59
|
+
const match = code.match(/^(?:create\s+table|alter\s+table|create\s+(?:unique\s+)?index\s+\S+\s+on)\s+(?:([`"])([^`"]+)\1|\[([^\]]+)\])/i);
|
|
60
|
+
return match?.[2] ?? match?.[3] ?? null;
|
|
61
|
+
};
|
|
62
|
+
/** Kysely over an in-memory node:sqlite database created with `ddl`. */
|
|
63
|
+
export const sqliteStand = ({ ddl }) => {
|
|
64
|
+
const sync = openSqlite(':memory:');
|
|
65
|
+
sync.exec(ddl);
|
|
66
|
+
const db = new Kysely({ dialect: nodeSqliteDialect(sync) });
|
|
67
|
+
return Promise.resolve({
|
|
68
|
+
db,
|
|
69
|
+
query: async (statement) => sync.prepare(statement).all(),
|
|
70
|
+
cleanup: async () => {
|
|
71
|
+
await db.destroy();
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const DEFAULT_DATABASE = 'dialect_stand';
|
|
76
|
+
/** A fresh Postgres database (not a schema) created with `ddl`. */
|
|
77
|
+
export const postgresStand = async (url, { ddl, database = DEFAULT_DATABASE }) => {
|
|
78
|
+
const { Client, Pool } = await import('pg');
|
|
79
|
+
const admin = new Client({ connectionString: url });
|
|
80
|
+
await admin.connect();
|
|
81
|
+
await admin.query(`drop database if exists ${database} with (force)`);
|
|
82
|
+
await admin.query(`create database ${database}`);
|
|
83
|
+
await admin.end();
|
|
84
|
+
const target = new URL(url);
|
|
85
|
+
target.pathname = `/${database}`;
|
|
86
|
+
const connectionString = target.toString();
|
|
87
|
+
const setup = new Client({ connectionString });
|
|
88
|
+
await setup.connect();
|
|
89
|
+
await setup.query(ddl);
|
|
90
|
+
await setup.end();
|
|
91
|
+
const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString }) }) });
|
|
92
|
+
return {
|
|
93
|
+
db,
|
|
94
|
+
url: connectionString,
|
|
95
|
+
cleanup: async () => {
|
|
96
|
+
await db.destroy();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/** A fresh MariaDB database (utf8mb4) created with `ddl`. */
|
|
101
|
+
export const mariadbStand = async (url, { ddl, database = DEFAULT_DATABASE }) => {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
const base = {
|
|
104
|
+
host: parsed.hostname,
|
|
105
|
+
port: Number(parsed.port || 3306),
|
|
106
|
+
user: decodeURIComponent(parsed.username),
|
|
107
|
+
password: decodeURIComponent(parsed.password),
|
|
108
|
+
};
|
|
109
|
+
const promise = (await import('mysql2/promise')).default;
|
|
110
|
+
const setup = await promise.createConnection({ ...base, multipleStatements: true });
|
|
111
|
+
await setup.query(`drop database if exists ${database}`);
|
|
112
|
+
await setup.query(`create database ${database} character set utf8mb4 collate utf8mb4_unicode_ci`);
|
|
113
|
+
await setup.query(`use ${database}`);
|
|
114
|
+
await setup.query(ddl);
|
|
115
|
+
await setup.end();
|
|
116
|
+
// Kysely's MySQL driver speaks the callback API of mysql2, not the promise one.
|
|
117
|
+
const mysql = (await import('mysql2')).default;
|
|
118
|
+
const pool = mysql.createPool({ ...base, database, connectionLimit: 4 });
|
|
119
|
+
const db = new Kysely({ dialect: new MysqlDialect({ pool }) });
|
|
120
|
+
return {
|
|
121
|
+
db,
|
|
122
|
+
url,
|
|
123
|
+
cleanup: async () => {
|
|
124
|
+
await db.destroy();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* A string column's type as the generator maps it for this dialect — a test that adds a
|
|
130
|
+
* column by hand must not re-invent the type map (MSSQL's `varchar` is not Unicode and
|
|
131
|
+
* silently mangles text; task:3994).
|
|
132
|
+
*/
|
|
133
|
+
export const stringColumnType = (dialect, length) => mapType({ type: 'string', length }, dialect);
|
|
134
|
+
/**
|
|
135
|
+
* A fresh SQL Server database created with `ddl` (task:3994: MSSQL became a target, so it
|
|
136
|
+
* needs the same stand as the other engines). The DSN is the ADO form `openDb` expects.
|
|
137
|
+
*/
|
|
138
|
+
export const mssqlStand = async (dsn, { ddl, database = DEFAULT_DATABASE }) => {
|
|
139
|
+
const { openDb } = await import('../../dbProvider.js');
|
|
140
|
+
const { sql } = await import('kysely');
|
|
141
|
+
const onDatabase = (name) => dsn.replace(/(database|initial catalog)=[^;]*/i, `Database=${name}`);
|
|
142
|
+
const master = await openDb({ dialect: 'mssql', dsn: onDatabase('master') });
|
|
143
|
+
try {
|
|
144
|
+
await sql.raw(`if db_id('${database}') is not null begin alter database [${database}] set single_user with rollback immediate; drop database [${database}]; end`).execute(master);
|
|
145
|
+
await sql.raw(`create database [${database}]`).execute(master);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await master.destroy();
|
|
149
|
+
}
|
|
150
|
+
const db = await openDb({ dialect: 'mssql', dsn: onDatabase(database) });
|
|
151
|
+
const text = ddl.split('\n').filter((line) => !line.trim().startsWith('--')).join('\n');
|
|
152
|
+
for (const statement of text.split(/;\s*\n/).map((part) => part.trim()).filter(Boolean)) {
|
|
153
|
+
await sql.raw(statement).execute(db);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
db,
|
|
157
|
+
url: onDatabase(database),
|
|
158
|
+
cleanup: async () => {
|
|
159
|
+
await db.destroy();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The fixed capture kinds (plan:50 Q7.1); a page set must match this exactly. */
|
|
2
|
+
export const PAGE_KINDS: string[];
|
|
3
|
+
export function loadPageSet(source?: any): Promise<any[]>;
|
|
4
|
+
export function stripCacheBust(url: any): any;
|
|
5
|
+
export function normalizeText(text: any, { origin, contentType }?: {
|
|
6
|
+
contentType?: string;
|
|
7
|
+
}): any;
|
|
8
|
+
export function normalizeHtml(html: any, { origin }?: {}): any;
|
|
9
|
+
export function captureSite({ origin, pages, fetchImpl, timeoutMs }: {
|
|
10
|
+
origin: any;
|
|
11
|
+
pages?: any;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
origin: any;
|
|
16
|
+
pages: ({
|
|
17
|
+
name: any;
|
|
18
|
+
kind: any;
|
|
19
|
+
path: any;
|
|
20
|
+
status: number;
|
|
21
|
+
location: any;
|
|
22
|
+
error: any;
|
|
23
|
+
body: any;
|
|
24
|
+
} | {
|
|
25
|
+
name: any;
|
|
26
|
+
kind: any;
|
|
27
|
+
path: any;
|
|
28
|
+
status: any;
|
|
29
|
+
location: any;
|
|
30
|
+
error: string;
|
|
31
|
+
body: string;
|
|
32
|
+
})[];
|
|
33
|
+
}>;
|
|
34
|
+
export function diffSnapshots(baseline: any, target: any): {
|
|
35
|
+
name: any;
|
|
36
|
+
kind: string;
|
|
37
|
+
path: any;
|
|
38
|
+
lines: string[];
|
|
39
|
+
}[];
|
|
40
|
+
export function formatReport({ baseline, target, diffs }: {
|
|
41
|
+
baseline: any;
|
|
42
|
+
target: any;
|
|
43
|
+
diffs: any;
|
|
44
|
+
}): string;
|