feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21)
- Helfer forSystem(prisma) in prisma-tenant.extension.ts (Array-Form, setzt app.system_context='true' und die beiden anderen Variablen ausdruecklich leer); forTenant()/withTenantTransaction() setzen app.system_context='' als Literal (4 neue Spec-Tests) - Migration 20260914120000_rls_system_context_read: is_system_context() (COALESCE, STABLE) und system_read_policy FOR SELECT auf DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch — lokal angewendet (36 Migrationen, pg_proc 1, 5 system_read_policy, 34 Regeln) - migration-sql.spec.ts: describe-Block fuer die neue Migration (6 Tests) - rls-scratch-check.mjs: Funktion aus der Migration geschnitten, forSystemQuery/buildInlineSystemClient, Reset in forTenantQuery/ buildInlineExtendedClient, runSystemContextChecks (4 Funktionsfaelle + 9 Kennungen DkvModuleConfig) -> Alle 216 Pruefungen bestanden - rls-access-inventory.spec.ts: fuenfte Erkennungsform const X = forSystem(, Stand system-gebunden mit Vorrangregel, FORSYSTEM_ALLOWED_CALL_SITES (exakte Zahl je Datei, 3 Tests), Proben C/D/E - DKV: loadActiveConfigsForScheduler() ueber forSystem (findMany isActive, CONFIG_SAFE_SELECT, orderBy tenantId); DkvSchedulerService mit Auftrag je Mandant dkv-inbox-poll:<tenantId>, activeTenantId ersatzlos entfernt, setInterval/stopJob je Mandant, registeredTenantIds(); Controller stopJob(tenantId); neue dkv-scheduler.service.spec.ts (7 Tests), dkv.service.spec.ts Tests 6/7 umgestellt - Klassifikation: dkv.service.ts/dkvModuleConfig system-gebunden, Header mit fuenfter Erkennungsform und viertem Stand-Wert - Baseline: 63 Dateien / 1051 Tests, tsc 0, Werkzeug 216 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -164,6 +164,27 @@ async function setupScratchDatabase(adminUrl) {
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT EXECUTE ON FUNCTION current_user_id() TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
|
||||
// Systemkontext (Etappe 3c, 260914-eym): is_system_context() wird aus
|
||||
// der Migration 20260914120000_rls_system_context_read GESCHNITTEN,
|
||||
// nicht getippt (T-EYM-07) — fehlt Migration oder Funktion, bricht das
|
||||
// Werkzeug hier ab, statt mit einem geratenen Funktionstext zu messen.
|
||||
const systemContextMigrationSql = readRlsSystemContextMigrationSql();
|
||||
if (!systemContextMigrationSql) {
|
||||
fail(
|
||||
'Migration "_rls_system_context_read" nicht gefunden — is_system_context() kann nicht geschnitten werden.',
|
||||
);
|
||||
}
|
||||
const isSystemContextFunctionSql = extractIsSystemContextFunctionSql(systemContextMigrationSql);
|
||||
if (!isSystemContextFunctionSql) {
|
||||
fail(
|
||||
'CREATE OR REPLACE FUNCTION is_system_context() nicht in der Systemkontext-Migration gefunden.',
|
||||
);
|
||||
}
|
||||
await db.$executeRawUnsafe(isSystemContextFunctionSql);
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT EXECUTE ON FUNCTION is_system_context() TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,7 +211,20 @@ function report(results, kennung, passed, detail) {
|
||||
* gemeinsamen Verbindung.
|
||||
*/
|
||||
async function forTenantQuery(prisma, tenantId, queryFn, userId) {
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true)`;
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true), set_config('app.system_context', '', true)`;
|
||||
const [, result] = await prisma.$transaction([setContext, queryFn(prisma)]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spiegelbildlich zu `forSystem()` in apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
* (Etappe 3c, 260914-eym) — bei jeder Aenderung dort HIER nachziehen: EINE
|
||||
* getaggte Anweisung setzt `app.system_context = 'true'` und AUSDRUECKLICH
|
||||
* `app.current_tenant = ''` und `app.current_user = ''`, alle drei als
|
||||
* Literale; danach die Abfrage in derselben Array-Transaktion.
|
||||
*/
|
||||
async function forSystemQuery(prisma, queryFn) {
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true)`;
|
||||
const [, result] = await prisma.$transaction([setContext, queryFn(prisma)]);
|
||||
return result;
|
||||
}
|
||||
@@ -512,6 +546,42 @@ function extractCurrentUserIdFunctionSql(migrationSql) {
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die Migration des Systemkontexts (Etappe 3c, 260914-eym, Dateiname
|
||||
* endet auf "_rls_system_context_read"). Die Funktion `is_system_context()`
|
||||
* und die fuenf `system_read_policy`-Regeln MUESSEN aus dieser Datei
|
||||
* geschnitten werden, nicht getippt (T-EYM-07, Muster
|
||||
* readRlsUserDimensionMigrationSql).
|
||||
*/
|
||||
function readRlsSystemContextMigrationSql() {
|
||||
const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.endsWith('_rls_system_context_read'))
|
||||
.map((entry) => entry.name);
|
||||
if (dirs.length !== 1) return null;
|
||||
return readFileSync(join(MIGRATIONS_DIR, dirs[0], 'migration.sql'), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schneidet die Definition von `is_system_context()` wortgleich aus der
|
||||
* Systemkontext-Migration. `null`, wenn nichts gefunden wird — der Aufrufer
|
||||
* bricht dann ab, statt die Funktion selbst zu tippen.
|
||||
*/
|
||||
function extractIsSystemContextFunctionSql(migrationSql) {
|
||||
const re = /CREATE OR REPLACE FUNCTION is_system_context\(\)[\s\S]*?LANGUAGE sql STABLE;/;
|
||||
const match = migrationSql.match(re);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schneidet die `system_read_policy` EINER Tabelle wortgleich aus der
|
||||
* Systemkontext-Migration (Muster extractPolicySql, anderer Regelname).
|
||||
*/
|
||||
function extractSystemReadPolicySql(migrationSql, tableName) {
|
||||
const re = new RegExp(`CREATE POLICY system_read_policy ON "${tableName}"[\\s\\S]*?;`);
|
||||
const match = migrationSql.match(re);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260909-ipc) — misst die fuenf im Plan genannten Verhaltensweisen
|
||||
* des Bereichs ldap unter der Rolle ohne BYPASSRLS, mit den beiden Policies
|
||||
@@ -5271,6 +5341,290 @@ async function runUserDimensionChecks(adminUrl, scratchRoleUrl, results) {
|
||||
* Setzt auf die Tabelle "Group" auf, die runGroupsAreaChecks() bereits
|
||||
* angelegt und mit je einer Zeile fuer TENANT-A/TENANT-B befuellt hat.
|
||||
*/
|
||||
/**
|
||||
* Systemkontext (Etappe 3c, 260914-eym) — innere Routine je Tabelle, Muster
|
||||
* `runSingleRulePersonalTableCheck`: Wegwerf-Tabelle mit ALLEN skalaren
|
||||
* Spalten (Spaltenvergleich gegen schema.prisma als erste Pruefung mit
|
||||
* Abbruch, 260910-krx-Lehre), Mandantenregel WORTGLEICH aus ihrer
|
||||
* jeweiligen Migration, Systemregel WORTGLEICH aus der neuen Migration,
|
||||
* Messung ueber den GENERIERTEN Client. Neun Kennungen je Tabelle:
|
||||
*
|
||||
* <slug>-wegwerftabelle-deckt-alle-spalten-des-generierten-clients
|
||||
* <slug>-ungebunden-null-zeilen (roher Client: 0 Zeilen)
|
||||
* <slug>-systemkontext-sieht-beide-mandanten (zu-wenig-Richtung, T-EYM-04)
|
||||
* <slug>-systemkontext-insert-abgewiesen-42501 (zu-viel-Richtung, T-EYM-02)
|
||||
* <slug>-systemkontext-updatemany-count-0
|
||||
* <slug>-systemkontext-deletemany-count-0
|
||||
* <slug>-fortenant-a-nach-systemkontext-nur-a (kein Erben, T-EYM-03)
|
||||
* <slug>-is-system-context-unter-fortenant-false
|
||||
* <slug>-pg-policies-genau-eine-system-read-policy-select
|
||||
*
|
||||
* Der Aufrufer reicht `tenantPolicySql` bereits geschnitten herein (jede
|
||||
* Tabelle hat ihre eigene Quellmigration); `dropTable=false` laesst eine
|
||||
* Elterntabelle stehen, auf die eine Folgetabelle per Join zeigt.
|
||||
*/
|
||||
async function runSystemContextTableCheck(config) {
|
||||
const {
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug,
|
||||
tableName,
|
||||
modelName,
|
||||
tenantPolicySql,
|
||||
systemContextMigrationSql,
|
||||
createTableSql,
|
||||
seedSql,
|
||||
tenantOfRow,
|
||||
createAttemptData,
|
||||
updateManyData,
|
||||
} = config;
|
||||
|
||||
const systemPolicySql = extractSystemReadPolicySql(systemContextMigrationSql, tableName);
|
||||
if (!systemPolicySql) {
|
||||
report(
|
||||
results,
|
||||
`${slug}-system-read-policy-aus-migration-gefunden`,
|
||||
false,
|
||||
`CREATE POLICY system_read_policy ON "${tableName}" nicht in der Systemkontext-Migration (20260914120000) gefunden`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!tenantPolicySql) {
|
||||
report(
|
||||
results,
|
||||
`${slug}-tenant-isolation-policy-aus-migration-gefunden`,
|
||||
false,
|
||||
`CREATE POLICY tenant_isolation_policy ON "${tableName}" nicht in der zugehoerigen Migration gefunden`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scratchAdminUrl = urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString();
|
||||
await withAdminPrisma(scratchAdminUrl, async (db) => {
|
||||
await db.$executeRawUnsafe(`DROP TABLE IF EXISTS "${tableName}" CASCADE;`);
|
||||
await db.$executeRawUnsafe(createTableSql);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "${tableName}" ENABLE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "${tableName}" FORCE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(tenantPolicySql);
|
||||
await db.$executeRawUnsafe(systemPolicySql);
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT SELECT, INSERT, UPDATE, DELETE ON "${tableName}" TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
await db.$executeRawUnsafe(seedSql);
|
||||
});
|
||||
|
||||
const schemaFields = readSchemaModelScalarFieldNames(modelName);
|
||||
const tableColumns = await withAdminPrisma(scratchAdminUrl, async (db) => {
|
||||
const rows = await db.$queryRawUnsafe(
|
||||
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${tableName}'`,
|
||||
);
|
||||
return rows.map((r) => r.column_name).sort();
|
||||
});
|
||||
const schemaFieldsSorted = [...schemaFields].sort();
|
||||
const columnsMatch =
|
||||
schemaFieldsSorted.length > 0 &&
|
||||
schemaFieldsSorted.length === tableColumns.length &&
|
||||
schemaFieldsSorted.every((f, i) => f === tableColumns[i]);
|
||||
report(
|
||||
results,
|
||||
`${slug}-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`,
|
||||
columnsMatch,
|
||||
`Schema-Felder aus schema.prisma (model ${modelName}, skalare Felder ohne Relation, ${schemaFieldsSorted.length}): ${JSON.stringify(schemaFieldsSorted)}; Spalten der Wegwerf-Tabelle (${tableColumns.length}): ${JSON.stringify(tableColumns)}`,
|
||||
);
|
||||
if (!columnsMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelAccessor = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
// 2: roher Client ohne jede Variable — 0 Zeilen (die Regel oeffnet
|
||||
// nichts, solange app.system_context nicht 'true' ist).
|
||||
const unboundRows = await prisma[modelAccessor].findMany();
|
||||
report(
|
||||
results,
|
||||
`${slug}-ungebunden-null-zeilen`,
|
||||
unboundRows.length === 0,
|
||||
`roher Client ${modelAccessor}.findMany() ohne Kontext liefert ${unboundRows.length} Zeile(n)`,
|
||||
);
|
||||
|
||||
// 3: Systemkontext sieht beide Mandanten (zu-wenig-Richtung).
|
||||
const systemClient = buildInlineSystemClient(prisma);
|
||||
const systemRows = await systemClient[modelAccessor].findMany();
|
||||
const seenTenants = [...new Set(systemRows.map((r) => tenantOfRow(r)))].sort();
|
||||
report(
|
||||
results,
|
||||
`${slug}-systemkontext-sieht-beide-mandanten`,
|
||||
seenTenants.length === 2 && seenTenants[0] === 'TENANT-A' && seenTenants[1] === 'TENANT-B',
|
||||
`system.${modelAccessor}.findMany() liefert ${systemRows.length} Zeile(n) aus Mandanten ${JSON.stringify(seenTenants)}`,
|
||||
);
|
||||
|
||||
// 4: INSERT unter Systemkontext — die Regel ist FOR SELECT, das
|
||||
// Schreiben faellt an der Mandantenregel durch (SQLSTATE 42501).
|
||||
let insertRejected = false;
|
||||
let insertDetail = '';
|
||||
try {
|
||||
const created = await systemClient[modelAccessor].create({ data: createAttemptData });
|
||||
insertDetail = `system.${modelAccessor}.create(${JSON.stringify(createAttemptData)}) ist NICHT fehlgeschlagen — angelegt: ${JSON.stringify(created?.id)}`;
|
||||
} catch (err) {
|
||||
const sqlState = sqlStateOf(err);
|
||||
const ctor = err?.constructor?.name ?? 'unbekannt';
|
||||
insertRejected = sqlState === '42501';
|
||||
insertDetail = `system.${modelAccessor}.create wirft ${ctor}, SQLSTATE ${sqlState ?? 'unbekannt'}: ${(err.message ?? '').toString().trim().split('\n').slice(-1)[0]}`;
|
||||
}
|
||||
report(results, `${slug}-systemkontext-insert-abgewiesen-42501`, insertRejected, insertDetail);
|
||||
|
||||
// 5: updateMany unter Systemkontext — count 0 (kein Mandant passt).
|
||||
const updated = await systemClient[modelAccessor].updateMany({ where: {}, data: updateManyData });
|
||||
report(
|
||||
results,
|
||||
`${slug}-systemkontext-updatemany-count-0`,
|
||||
updated.count === 0,
|
||||
`system.${modelAccessor}.updateMany({ where: {}, data: ${JSON.stringify(updateManyData)} }) liefert count=${updated.count}`,
|
||||
);
|
||||
|
||||
// 6: deleteMany unter Systemkontext — count 0.
|
||||
const deleted = await systemClient[modelAccessor].deleteMany({ where: {} });
|
||||
const rowsAfterDelete = await withAdminPrisma(scratchAdminUrl, async (db) => {
|
||||
const rows = await db.$queryRawUnsafe(`SELECT count(*)::int AS c FROM "${tableName}"`);
|
||||
return rows[0].c;
|
||||
});
|
||||
report(
|
||||
results,
|
||||
`${slug}-systemkontext-deletemany-count-0`,
|
||||
deleted.count === 0 && rowsAfterDelete === 2,
|
||||
`system.${modelAccessor}.deleteMany({}) liefert count=${deleted.count}; Zeilen danach (Wartungsrolle): ${rowsAfterDelete}`,
|
||||
);
|
||||
|
||||
// 7: forTenant(A) unmittelbar nach dem Systemkontext auf DEMSELBEN
|
||||
// Client — nur A (kein Erben, T-EYM-03).
|
||||
await systemClient[modelAccessor].findMany();
|
||||
const boundA = buildInlineExtendedClient(prisma, 'TENANT-A');
|
||||
const rowsA = await boundA[modelAccessor].findMany();
|
||||
const tenantsA = [...new Set(rowsA.map((r) => tenantOfRow(r)))];
|
||||
report(
|
||||
results,
|
||||
`${slug}-fortenant-a-nach-systemkontext-nur-a`,
|
||||
rowsA.length === 1 && tenantsA.length === 1 && tenantsA[0] === 'TENANT-A',
|
||||
`bound(TENANT-A).${modelAccessor}.findMany() unmittelbar nach system.findMany() auf demselben Client liefert ${rowsA.length} Zeile(n) aus ${JSON.stringify(tenantsA)}`,
|
||||
);
|
||||
|
||||
// 8: is_system_context() innerhalb der forTenant-Transaktion — false.
|
||||
const [, isSystemRows] = await prisma.$transaction([
|
||||
prisma.$executeRaw`SELECT set_config('app.current_tenant', 'TENANT-A', true), set_config('app.current_user', '', true), set_config('app.system_context', '', true)`,
|
||||
prisma.$queryRaw`SELECT is_system_context() AS v, current_setting('app.system_context', true) AS raw`,
|
||||
]);
|
||||
report(
|
||||
results,
|
||||
`${slug}-is-system-context-unter-fortenant-false`,
|
||||
isSystemRows[0].v === false,
|
||||
`is_system_context() innerhalb der forTenant(TENANT-A)-Transaktion = ${JSON.stringify(isSystemRows[0].v)} (Rohwert ${JSON.stringify(isSystemRows[0].raw)})`,
|
||||
);
|
||||
|
||||
// 9: pg_policies unter der Wegwerf-Rolle — genau eine system_read_policy, SELECT.
|
||||
const policyRows = await prisma.$queryRaw`SELECT policyname, cmd, permissive, qual FROM pg_policies WHERE schemaname = 'public' AND tablename = ${tableName} AND policyname = 'system_read_policy'`;
|
||||
report(
|
||||
results,
|
||||
`${slug}-pg-policies-genau-eine-system-read-policy-select`,
|
||||
policyRows.length === 1 && policyRows[0].cmd === 'SELECT' && policyRows[0].permissive === 'PERMISSIVE',
|
||||
`pg_policies fuer "${tableName}" (system_read_policy): ${JSON.stringify(policyRows)}`,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Systemkontext (Etappe 3c, 260914-eym) — misst zuerst die vier
|
||||
* Funktionsfaelle von `is_system_context()` unter der Wegwerf-Rolle, dann
|
||||
* je betroffener Tabelle die neun Wahrheiten ueber die innere Routine.
|
||||
* Laeuft NACH runUserDimensionChecks() und VOR runConcurrencyProbe() (siehe
|
||||
* Aufrufkette in main()); legt seine Wegwerf-Tabellen selbst neu an und
|
||||
* setzt auf keiner Tabelle eines anderen Abschnitts auf.
|
||||
*/
|
||||
async function runSystemContextChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const systemContextMigrationSql = readRlsSystemContextMigrationSql();
|
||||
if (!systemContextMigrationSql) {
|
||||
report(results, 'system-context-migration-gefunden', false, 'Migration "_rls_system_context_read" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
const remainingTablesMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
if (!remainingTablesMigrationSql) {
|
||||
report(results, 'system-context-remaining-tables-migration-gefunden', false, 'Migration "_rls_remaining_tenant_tables" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vier Funktionsfaelle, je in einer eigenen Transaktion.
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
const unsetRows = await prisma.$queryRaw`SELECT is_system_context() AS v, current_setting('app.system_context', true) AS raw`;
|
||||
report(
|
||||
results,
|
||||
'is-system-context-ungesetzt-false',
|
||||
unsetRows[0].v === false,
|
||||
`ohne gesetzte Variable: is_system_context() = ${JSON.stringify(unsetRows[0].v)} (Rohwert ${JSON.stringify(unsetRows[0].raw)}) — die Vorher-Pruefung ohne-kontext-leer in rls-preflight.mjs bleibt gueltig`,
|
||||
);
|
||||
|
||||
const probeValue = async (value) => {
|
||||
const [, rows] = await prisma.$transaction([
|
||||
prisma.$executeRaw`SELECT set_config('app.system_context', ${value}, true)`,
|
||||
prisma.$queryRaw`SELECT is_system_context() AS v`,
|
||||
]);
|
||||
return rows[0].v;
|
||||
};
|
||||
const emptyValue = await probeValue('');
|
||||
report(results, 'is-system-context-leer-false', emptyValue === false, `nach set_config('app.system_context', '', true): ${JSON.stringify(emptyValue)}`);
|
||||
const trueValue = await probeValue('true');
|
||||
report(results, 'is-system-context-true-true', trueValue === true, `nach set_config('app.system_context', 'true', true): ${JSON.stringify(trueValue)}`);
|
||||
const foreignValue = await probeValue('yes');
|
||||
report(results, 'is-system-context-fremdwert-false', foreignValue === false, `nach set_config('app.system_context', 'yes', true): ${JSON.stringify(foreignValue)}`);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
// DkvModuleConfig — Mandantenregel aus 20260909140000_rls_remaining_tenant_tables,
|
||||
// alle 16 skalaren Spalten des Modells.
|
||||
await runSystemContextTableCheck({
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug: 'dkvmoduleconfig',
|
||||
tableName: 'DkvModuleConfig',
|
||||
modelName: 'DkvModuleConfig',
|
||||
tenantPolicySql: extractPolicySql(remainingTablesMigrationSql, 'DkvModuleConfig'),
|
||||
systemContextMigrationSql,
|
||||
createTableSql: `
|
||||
CREATE TABLE "DkvModuleConfig" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL UNIQUE,
|
||||
protocol text NOT NULL DEFAULT 'imap',
|
||||
host text,
|
||||
port integer,
|
||||
encryption text NOT NULL DEFAULT 'ssl-tls',
|
||||
folder text NOT NULL DEFAULT 'INBOX',
|
||||
"senderFilter" text,
|
||||
"pollIntervalMin" integer NOT NULL DEFAULT 60,
|
||||
"isActive" boolean NOT NULL DEFAULT false,
|
||||
"exportRecipient" text,
|
||||
"vehicleFormatString" text NOT NULL DEFAULT '{Marke}/{Modell}/{Kennzeichen}',
|
||||
domain text,
|
||||
"encryptedInboxCreds" text,
|
||||
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
seedSql: `
|
||||
INSERT INTO "DkvModuleConfig" (id, "tenantId", "isActive", "pollIntervalMin") VALUES
|
||||
('cfg-a', 'TENANT-A', true, 15),
|
||||
('cfg-b', 'TENANT-B', true, 60);
|
||||
`,
|
||||
tenantOfRow: (row) => row.tenantId,
|
||||
createAttemptData: { id: 'cfg-system-schreibversuch', tenantId: 'TENANT-A', isActive: true },
|
||||
updateManyData: { folder: 'SYSTEM-SCHREIBVERSUCH' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spiegelbildlich zu `forTenant()` in apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
* — bei jeder Aenderung dort HIER nachziehen. Seit Etappe 3b (260911-nke)
|
||||
@@ -5282,7 +5636,25 @@ function buildInlineExtendedClient(prisma, tenantId, userId) {
|
||||
return prisma.$extends({
|
||||
query: {
|
||||
$allOperations({ args, query }) {
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true)`;
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.current_tenant', ${tenantId}, true), set_config('app.current_user', ${userId ?? ''}, true), set_config('app.system_context', '', true)`;
|
||||
return prisma.$transaction([setContext, query(args)]).then((res) => res[1]);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spiegelbildlich zu `forSystem()` in apps/api/src/prisma/prisma-tenant.extension.ts
|
||||
* (Etappe 3c, 260914-eym) — bei jeder Aenderung dort HIER nachziehen. Der
|
||||
* Systemkontext ueber den GENERIERTEN Client: `app.system_context = 'true'`,
|
||||
* die beiden anderen Variablen ausdruecklich leer, alles Literale, Array-Form
|
||||
* von $transaction.
|
||||
*/
|
||||
function buildInlineSystemClient(prisma) {
|
||||
return prisma.$extends({
|
||||
query: {
|
||||
$allOperations({ args, query }) {
|
||||
const setContext = prisma.$executeRaw`SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true)`;
|
||||
return prisma.$transaction([setContext, query(args)]).then((res) => res[1]);
|
||||
},
|
||||
},
|
||||
@@ -5510,6 +5882,7 @@ async function main() {
|
||||
await runSettingsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
|
||||
await runUserDimensionChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runSystemContextChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runConcurrencyProbe(scratchRoleUrlString, results);
|
||||
} finally {
|
||||
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
|
||||
|
||||
Reference in New Issue
Block a user