build(server): hold SQLite-only SQL spellings to a shrinking baseline

ESLint keeps raw SQL out of the repositories, but nothing looked at the rest
of src/: the entities, the domain services, the src/db lifecycle files or a
migration added from now on. scripts/dialect-lint.mjs (npm run lint:dialect)
reads every string and template literal under src/, skipping comments, and
counts INSERT OR IGNORE/REPLACE, datetime(), strftime, json_extract, GLOB,
last_insert_rowid, AUTOINCREMENT, PRAGMA outside the lifecycle files, ||
concatenation in SQL and .insertId reads. scripts/dialect-baseline.json
holds today's thirteen hits; the counts may only go down, like lint:size.
The dialect layer is not scanned, and neither are the migrations that have
shipped.

The four seeders that still spelled INSERT OR IGNORE (addons, photo and
document providers, the schema_version marker) are Kysely inserts that do
nothing on conflict now, so a re-run still leaves an operator's rows alone.
They leave the SQL-string exemption list in eslint.config.mjs and their pins
in the selector test.
This commit is contained in:
Maurice
2026-10-08 15:21:17 +02:00
parent c524454f47
commit 39bb8f9619
14 changed files with 689 additions and 64 deletions
+12 -8
View File
@@ -1,5 +1,7 @@
import type { EntityManager } from '@mikro-orm/core';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { Seeder } from '@mikro-orm/seeder';
import type { DB } from '../kysely/db';
interface AddonRow {
id: string;
@@ -154,18 +156,20 @@ const DEFAULT_ADDONS: AddonRow[] = [
/**
* Seeds the addon shelf.
*
* `INSERT OR IGNORE` per row, not a table-level emptiness check: an operator who
* has toggled an addon keeps their choice, and a release that adds a new addon
* still gets its tile on an existing install.
* One insert per row that does nothing when the id is already there, not a
* table-level emptiness check: an operator who has toggled an addon keeps their
* choice, and a release that adds a new addon still gets its tile on an
* existing install.
*/
export class AddonSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const connection = em.getConnection();
const db = (em as SqlEntityManager).getKysely<Pick<DB, 'addons'>>();
for (const addon of DEFAULT_ADDONS) {
await connection.execute(
'INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)',
[addon.id, addon.name, addon.description, addon.type, addon.icon, addon.enabled, addon.sort_order],
);
await db
.insertInto('addons')
.values({ ...addon })
.onConflict((oc) => oc.doNothing())
.execute();
}
console.log('Default addons seeded');
}
+25 -12
View File
@@ -1,32 +1,45 @@
import { DOCUMENT_PROVIDERS, DOCUMENT_PROVIDER_FIELDS } from '../document-provider-seed';
import type { EntityManager } from '@mikro-orm/core';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { Seeder } from '@mikro-orm/seeder';
import type { DB } from '../kysely/db';
/**
* Seeds the document providers.
*
* The rows come from `document-provider-seed.ts`, the same constants the
* migration that introduces the tables uses, so a fresh install and an upgraded
* one end up with identical rows.
* one end up with identical rows. A row that is already there is left alone
* (the insert does nothing on conflict), so an operator's edits survive.
*/
export class DocumentProviderSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const connection = em.getConnection();
const db = (em as SqlEntityManager).getKysely<Pick<DB, 'document_providers' | 'document_provider_fields'>>();
for (const p of DOCUMENT_PROVIDERS) {
await connection.execute(
'INSERT OR IGNORE INTO document_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, 0, ?)',
[p.id, p.name, p.description, p.icon, p.sort_order],
);
await db
.insertInto('document_providers')
.values({ id: p.id, name: p.name, description: p.description, icon: p.icon, enabled: 0, sort_order: p.sort_order })
.onConflict((oc) => oc.doNothing())
.execute();
}
for (const f of DOCUMENT_PROVIDER_FIELDS) {
await connection.execute(
`INSERT OR IGNORE INTO document_provider_fields
(provider_id, field_key, label, input_type, placeholder, hint, required, secret, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[f.provider_id, f.field_key, f.label, f.input_type, f.placeholder, f.hint, f.required, f.secret, f.sort_order],
);
await db
.insertInto('document_provider_fields')
.values({
provider_id: f.provider_id,
field_key: f.field_key,
label: f.label,
input_type: f.input_type,
placeholder: f.placeholder,
hint: f.hint,
required: f.required,
secret: f.secret,
sort_order: f.sort_order,
})
.onConflict((oc) => oc.doNothing())
.execute();
}
}
}
+26 -24
View File
@@ -1,5 +1,7 @@
import type { EntityManager } from '@mikro-orm/core';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { Seeder } from '@mikro-orm/seeder';
import type { DB } from '../kysely/db';
interface PhotoProviderRow {
id: string;
@@ -157,37 +159,37 @@ const FIELDS: PhotoProviderFieldRow[] = [
},
];
/** Seeds the photo providers and the fields their connection form asks for. */
/** Seeds the photo providers and the fields their connection form asks for, leaving a row that is already there alone. */
export class PhotoProviderSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const connection = em.getConnection();
const db = (em as SqlEntityManager).getKysely<Pick<DB, 'photo_providers' | 'photo_provider_fields'>>();
for (const p of PROVIDERS) {
await connection.execute(
'INSERT OR IGNORE INTO photo_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
[p.id, p.name, p.description, p.icon, p.enabled, p.sort_order],
);
await db
.insertInto('photo_providers')
.values({ id: p.id, name: p.name, description: p.description, icon: p.icon, enabled: p.enabled, sort_order: p.sort_order })
.onConflict((oc) => oc.doNothing())
.execute();
}
for (const f of FIELDS) {
await connection.execute(
`INSERT OR IGNORE INTO photo_provider_fields
(provider_id, field_key, label, input_type, placeholder, hint, required, secret, settings_key, payload_key, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
f.provider_id,
f.field_key,
f.label,
f.input_type,
f.placeholder,
f.hint,
f.required,
f.secret,
f.settings_key,
f.payload_key,
f.sort_order,
],
);
await db
.insertInto('photo_provider_fields')
.values({
provider_id: f.provider_id,
field_key: f.field_key,
label: f.label,
input_type: f.input_type,
placeholder: f.placeholder,
hint: f.hint,
required: f.required,
secret: f.secret,
settings_key: f.settings_key,
payload_key: f.payload_key,
sort_order: f.sort_order,
})
.onConflict((oc) => oc.doNothing())
.execute();
}
}
}
+9 -7
View File
@@ -1,5 +1,7 @@
import type { EntityManager } from '@mikro-orm/core';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { Seeder } from '@mikro-orm/seeder';
import type { DB } from '../kysely/db';
/**
* The version the hand-written `db/migrations.ts` chain ended on — the highest
@@ -16,17 +18,17 @@ const LEGACY_SCHEMA_VERSION = 258;
* `mikro_orm_migrations` now — but it is still an entity and still carries the
* row an installation upgraded from the hand-written chain would have.
*
* `INSERT OR IGNORE`, like every other seeder here: seeding runs again whenever
* An insert that does nothing on conflict, like every other seeder here: seeding runs again whenever
* the schema bootstrap does, and a backup restore re-runs it against a database
* that already has this row.
*/
export class SchemaVersionSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
const connection = em.getConnection();
await connection.execute('INSERT OR IGNORE INTO schema_version (id, version) VALUES (?, ?)', [
1,
LEGACY_SCHEMA_VERSION,
]);
await (em as SqlEntityManager)
.getKysely<Pick<DB, 'schema_version'>>()
.insertInto('schema_version')
.values({ id: 1, version: LEGACY_SCHEMA_VERSION })
.onConflict((oc) => oc.doNothing())
.execute();
}
}