10847 lines
421 KiB
JavaScript
10847 lines
421 KiB
JavaScript
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
|
||
const scanRoots = ["apps", "packages"];
|
||
const ignoredScanSegments = new Set(["node_modules", "dist", ".vite", "coverage", ".next", "generated"]);
|
||
const sourceLikeFilePattern = /\.(?:ts|tsx|js|jsx|mjs|cjs|json|sql|prisma)$/;
|
||
const prismaMigrationPrefix = "apps/api/prisma/migrations/";
|
||
const generatedPrismaPrefix = "apps/api/src/generated/prisma/";
|
||
const stateTransitionModulePrefix = "apps/api/src/modules/state-transition/";
|
||
const auditModulePrefix = "apps/api/src/modules/audit/";
|
||
const jobsModulePrefix = "apps/api/src/modules/jobs/";
|
||
const jobExecutionStorePath = "apps/api/src/modules/jobs/index.ts";
|
||
const initialAuditLogAppendOnlyFunctionMigration = "apps/api/prisma/migrations/20260601040253_s1_app_foundation/migration.sql";
|
||
const historicalPrismaSchemaTest = "apps/api/src/prisma.schema.spec.ts";
|
||
const auditTriggerAssertionFiles = new Set([historicalPrismaSchemaTest, "apps/api/src/modules/audit/audit.spec.ts"]);
|
||
const stateTransitionImportFixtureTests = new Set([
|
||
"apps/api/src/modules/jobs/jobs.spec.ts",
|
||
"apps/api/src/modules/jobs/jobs.api.spec.ts",
|
||
"apps/api/src/modules/projects/api-runtime.spec.ts",
|
||
"apps/api/src/modules/projects/projects.api.spec.ts"
|
||
]);
|
||
|
||
const laterPhaseDeniedTerms = [
|
||
"creation-agent/messages",
|
||
"compile-game-ir",
|
||
"web-runtime",
|
||
"mini-game-conversion",
|
||
"PromptRecord",
|
||
"AIGameDesignDraft",
|
||
"DesignSectionPatch",
|
||
"DesignSection",
|
||
"GameIRArtifact",
|
||
"GameIR",
|
||
"ConversionReport",
|
||
"ValidationReport",
|
||
"MiniGameProject",
|
||
"MiniGameCodeConversion",
|
||
"FeedItem",
|
||
"GameDailyStats",
|
||
"CreatorDailyStats",
|
||
"/events/batch"
|
||
];
|
||
|
||
const supportedPhases = new Set(["S1", "S2"]);
|
||
const s2ApprovedTerms = new Set([
|
||
"creation-agent/messages",
|
||
"compile-game-ir",
|
||
"PromptRecord",
|
||
"AIGameDesignDraft",
|
||
"DesignSectionPatch",
|
||
"DesignSection",
|
||
"GameIRArtifact",
|
||
"GameIR"
|
||
]);
|
||
const s2ApprovedTermDenyPathSegments = new Set([
|
||
"simulation",
|
||
"web-runtime",
|
||
"minigame-conversion",
|
||
"mini_game_conversion",
|
||
"publish-review-feed",
|
||
"publish_review_feed",
|
||
"mini-game-conversion",
|
||
"conversion",
|
||
"feed",
|
||
"runtime",
|
||
"publish",
|
||
"review"
|
||
]);
|
||
const s2ApprovedTermDenyPathFragments = ["/events/batch/"];
|
||
const s2ApprovedTermAllowPathPrefixes = [
|
||
"apps/api/src/modules/creation-agent/",
|
||
"apps/api/src/modules/ai-design/",
|
||
"apps/api/src/modules/workbench/",
|
||
"apps/api/src/modules/game-ir/",
|
||
"apps/api/src/modules/compile-game-ir/",
|
||
"apps/api/prisma/migrations/",
|
||
"apps/web/src/app/workbench/",
|
||
"apps/web/src/components/workbench/"
|
||
];
|
||
const s2ApprovedTermAllowExactPaths = new Set([
|
||
"apps/api/prisma/schema.prisma",
|
||
// Prisma schema contract tests需要直接断言 S2 设计模型及其 lineage 约束;
|
||
// 仍保持精确路径放行,避免普通 API 测试借用 S2 术语绕过阶段门禁。
|
||
"apps/api/src/prisma.schema.spec.ts",
|
||
"packages/shared-contracts/src/index.ts",
|
||
"packages/shared-contracts/src/index.spec.ts",
|
||
"packages/shared-contracts/src/workflow.ts",
|
||
"packages/shared-contracts/src/workflow.spec.ts",
|
||
"packages/shared-contracts/src/ai-design.ts",
|
||
"packages/shared-contracts/src/ai-design.spec.ts",
|
||
"packages/shared-contracts/src/game-ir.ts",
|
||
"packages/shared-contracts/src/game-ir.spec.ts"
|
||
]);
|
||
|
||
const harnessValidationContractTerms = new Set([
|
||
"AIGameDesignDraft",
|
||
"GameIR",
|
||
"ConversionReport",
|
||
"ValidationReport",
|
||
"MiniGameProject",
|
||
"MiniGameCodeConversion"
|
||
]);
|
||
|
||
const harnessClientContractWatchTerms = new Set(harnessValidationContractTerms);
|
||
const jobRuntimeFields = [
|
||
"status",
|
||
"attempts",
|
||
"nextRetryAt",
|
||
"errorCode",
|
||
"leaseToken",
|
||
"leasedBy",
|
||
"leaseExpiresAt",
|
||
"lockVersion",
|
||
"timeoutAt"
|
||
];
|
||
const rawSqlVariableResolutionMaxDepth = 6;
|
||
const dynamicSqlIdentifierMarker = "__HUJING_DYNAMIC_SQL_IDENTIFIER__";
|
||
const auditLogAppendOnlyFunctionName = "app_reject_audit_log_mutation";
|
||
const auditPrismaMutationMethods = ["update", "updateMany", "updateManyAndReturn", "upsert", "delete", "deleteMany"];
|
||
const guardedPrismaWriteMethods = ["create", "update", "upsert", "createMany", "updateMany", "delete", "deleteMany"];
|
||
const guardedPrismaWriteModels = ["gameVersion", "reviewRecord", "lifecycleEvent"];
|
||
const guardedStateSqlTables = ["GameVersion", "ReviewRecord", "LifecycleEvent"];
|
||
const jobPrismaWriteMethods = ["create", "createMany", "createManyAndReturn", "update", "updateMany", "updateManyAndReturn", "upsert"];
|
||
const jobPrismaDeleteMethods = ["delete", "deleteMany"];
|
||
|
||
const s1AnchorTerms = ["MainCreationAgentSession", "AgentTask", "ReviewRecord", "LifecycleEvent"];
|
||
const s2ApprovedS1AnchorTerms = new Set(["MainCreationAgentSession", "AgentTask"]);
|
||
const allowedStateTransitionImportPaths = new Set(["apps/api/src/modules/projects/index.ts"]);
|
||
const allowedStateTransitionImportPrefixes = [stateTransitionModulePrefix];
|
||
|
||
function normalizePath(filePath) {
|
||
return filePath.split(path.sep).join("/");
|
||
}
|
||
|
||
function isIgnoredScanPath(filePath) {
|
||
return filePath.split("/").some((segment) => ignoredScanSegments.has(segment));
|
||
}
|
||
|
||
function isHarnessClientTestPath(filePath) {
|
||
return /^packages\/harness-client\/.*\.(?:spec|test)\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath);
|
||
}
|
||
|
||
function escapeRegExp(value) {
|
||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
}
|
||
|
||
function sqlBareIdentifierPattern() {
|
||
return String.raw`[A-Za-z_][\w$]*`;
|
||
}
|
||
|
||
function sqlTriggerIdentifierPattern() {
|
||
// SQL 扫描会去掉双引号,quoted trigger name 里的连字符需作为同一 token fail-closed。
|
||
return String.raw`[^\s(),;]+`;
|
||
}
|
||
|
||
function validationContractReferenceSpans(content, term) {
|
||
const spans = [];
|
||
const pattern = new RegExp(`\\bvalidateContract\\s*\\(\\s*(["'])${escapeRegExp(term)}\\1`, "g");
|
||
for (const match of content.matchAll(pattern)) {
|
||
const termOffset = match[0].lastIndexOf(term);
|
||
if (typeof match.index === "number" && termOffset >= 0) {
|
||
spans.push({ start: match.index + termOffset, end: match.index + termOffset + term.length });
|
||
}
|
||
}
|
||
return spans;
|
||
}
|
||
|
||
function enclosingBraceSpan(content, index) {
|
||
const structure = maskCommentsOutsideStringLiterals(content);
|
||
let smallestSpan = null;
|
||
|
||
for (let cursor = 0; cursor < structure.length; cursor += 1) {
|
||
if (structure[cursor] !== "{") continue;
|
||
const closeBraceIndex = findMatchingBrace(structure, cursor);
|
||
if (closeBraceIndex === -1 || index < cursor || index > closeBraceIndex) continue;
|
||
|
||
const span = { start: cursor, end: closeBraceIndex + 1 };
|
||
if (!smallestSpan || span.end - span.start < smallestSpan.end - smallestSpan.start) {
|
||
smallestSpan = span;
|
||
}
|
||
}
|
||
|
||
return smallestSpan;
|
||
}
|
||
|
||
function hasHarnessCliContractArgumentContext(content, assertionIndex) {
|
||
const span = enclosingBraceSpan(content, assertionIndex);
|
||
const contextStart = Math.max(span?.start ?? 0, assertionIndex - 400);
|
||
const contextEnd = Math.min(span?.end ?? content.length, assertionIndex + 220);
|
||
const context = content.slice(contextStart, contextEnd);
|
||
|
||
// args[2] 只有在同一 runner 小窗口内已证明 args[0..1] 指向真实 harness CLI contract 调用时才可作为合同名断言。
|
||
return /\bexpect\s*\(\s*args\s*\.\s*slice\s*\(\s*0\s*,\s*2\s*\)\s*\)\s*\.\s*to(?:Strict)?Equal\s*\(\s*\[\s*(?:realHarnessCli|[^\]]*validate-harness\.mjs[^\]]*)\s*,\s*(["'])--contract\1\s*\]\s*\)/s.test(
|
||
context
|
||
);
|
||
}
|
||
|
||
function harnessCliContractArgumentAssertionSpans(content, term) {
|
||
const spans = [];
|
||
const pattern = new RegExp(
|
||
`\\bexpect\\s*\\(\\s*args\\s*\\[\\s*2\\s*\\]\\s*\\)\\s*\\.\\s*toBe\\s*\\(\\s*(["'])${escapeRegExp(term)}\\1\\s*\\)`,
|
||
"g"
|
||
);
|
||
for (const match of content.matchAll(pattern)) {
|
||
const termOffset = match[0].lastIndexOf(term);
|
||
if (typeof match.index === "number" && termOffset >= 0 && hasHarnessCliContractArgumentContext(content, match.index)) {
|
||
spans.push({ start: match.index + termOffset, end: match.index + termOffset + term.length });
|
||
}
|
||
}
|
||
return spans;
|
||
}
|
||
|
||
function isInSpan(index, spans) {
|
||
return spans.some((span) => index >= span.start && index < span.end);
|
||
}
|
||
|
||
function isHarnessGateValidationTestPath(filePath) {
|
||
return /^apps\/api\/src\/modules\/harness-gate\/.*\.(?:spec|test)\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath);
|
||
}
|
||
|
||
function isAllowedHarnessValidationContractReferenceAt(filePath, content, term, index) {
|
||
if (!harnessValidationContractTerms.has(term)) return false;
|
||
if (!isHarnessClientTestPath(filePath) && !isHarnessGateValidationTestPath(filePath)) return false;
|
||
if (isInSpan(index, validationContractReferenceSpans(content, term))) return true;
|
||
|
||
// harness-client 的 runner 断言 `args[2]` 是 `--contract` 的合同名;这是 CLI validation 证据。
|
||
return isHarnessClientTestPath(filePath) && isInSpan(index, harnessCliContractArgumentAssertionSpans(content, term));
|
||
}
|
||
|
||
function isAllowedHarnessClientContractReferenceAt(filePath, content, term, index) {
|
||
if (!isHarnessClientTestPath(filePath)) return false;
|
||
return isAllowedHarnessValidationContractReferenceAt(filePath, content, term, index);
|
||
}
|
||
|
||
function normalizeDeniedPathTerm(term) {
|
||
return term.split(path.sep).join("/").replace(/^\/+|\/+$/g, "");
|
||
}
|
||
|
||
function kebabCaseContractTerm(term) {
|
||
return term
|
||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
||
.toLowerCase();
|
||
}
|
||
|
||
function pathContainsLaterPhaseTerm(filePath, term) {
|
||
const deniedPathTerm = normalizeDeniedPathTerm(term);
|
||
if (!deniedPathTerm) return false;
|
||
|
||
const normalizedFilePath = normalizePath(filePath).replace(/^\/+|\/+$/g, "");
|
||
const pathWithBoundaries = `/${normalizedFilePath}/`;
|
||
|
||
// route/module fragment 要按路径边界匹配,同时覆盖同名单文件模块。
|
||
if (deniedPathTerm.includes("/")) {
|
||
const deniedSourceFilePattern = new RegExp(`(?:^|/)${escapeRegExp(deniedPathTerm)}${sourceLikeFilePattern.source}`);
|
||
return pathWithBoundaries.includes(`/${deniedPathTerm}/`) || deniedSourceFilePattern.test(normalizedFilePath);
|
||
}
|
||
|
||
// symbol-like term 在路径中按 segment/filename 命中,覆盖 AIGameDesignDraft.ts 这类文件名。
|
||
const pathTermVariants = new Set([deniedPathTerm, kebabCaseContractTerm(deniedPathTerm)]);
|
||
return normalizedFilePath
|
||
.split("/")
|
||
.some((segment) => [...pathTermVariants].some((pathTermVariant) => segment.includes(pathTermVariant)));
|
||
}
|
||
|
||
function contentDeniedTermVariants(term) {
|
||
if (!term.startsWith("/")) return [term];
|
||
|
||
// 内容扫描把同一路由片段的绝对/相对写法视为等价,避免 "/events/batch" 漏掉 "events/batch"。
|
||
const normalizedTerm = term.replace(/^\/+/, "");
|
||
return normalizedTerm ? [term, normalizedTerm] : [term];
|
||
}
|
||
|
||
function isTestFile(filePath) {
|
||
return /\.(?:spec|test)\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath);
|
||
}
|
||
|
||
function isHistoricalTestFile(filePath) {
|
||
return filePath === historicalPrismaSchemaTest;
|
||
}
|
||
|
||
function isGeneratedPrismaFile(filePath) {
|
||
return filePath.startsWith(generatedPrismaPrefix);
|
||
}
|
||
|
||
function isPrismaMigrationSqlPath(filePath) {
|
||
return filePath.startsWith(prismaMigrationPrefix) && filePath.endsWith("/migration.sql");
|
||
}
|
||
|
||
function auditLogAppendOnlyFunctionCreatePattern() {
|
||
const auditLogAppendOnlyFunction = sqlFunctionReferencePattern(auditLogAppendOnlyFunctionName);
|
||
|
||
return new RegExp(String.raw`\bCREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+${auditLogAppendOnlyFunction}\b`, "gi");
|
||
}
|
||
|
||
function auditLogAppendOnlyFunctionInstallPattern() {
|
||
const auditLogAppendOnlyFunction = sqlFunctionReferencePattern(auditLogAppendOnlyFunctionName);
|
||
const dollarQuote = String.raw`(?:\$\$|\$[A-Za-z_][\w$]*\$)`;
|
||
|
||
return new RegExp(
|
||
String.raw`\bCREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+${auditLogAppendOnlyFunction}\s+RETURNS\s+trigger\s+AS\s+(${dollarQuote})([\s\S]*?)\2\s+LANGUAGE\s+plpgsql\b`,
|
||
"gi"
|
||
);
|
||
}
|
||
|
||
function auditLogAppendOnlyFunctionCreateStatements(sqlSource) {
|
||
const sql = stripSqlComments(sqlSource);
|
||
return [...sql.matchAll(auditLogAppendOnlyFunctionCreatePattern())];
|
||
}
|
||
|
||
function auditLogAppendOnlyFunctionDefinitions(sqlSource) {
|
||
const sql = stripSqlComments(sqlSource);
|
||
return [...sql.matchAll(auditLogAppendOnlyFunctionInstallPattern())].map((match) => ({
|
||
isCreateOrReplace: Boolean(match[1]),
|
||
body: match[3]
|
||
}));
|
||
}
|
||
|
||
function isCanonicalAuditLogAppendOnlyFunctionBody(functionBody) {
|
||
// 允许 SQL 关键字大小写与空白变化,但函数体只能安装固定 append-only guard,不能夹带 RETURN/IF/EXECUTE 等语句。
|
||
return /^\s*BEGIN\s+RAISE\s+EXCEPTION\s+'AuditLog is append-only'\s+USING\s+ERRCODE\s*=\s*'42501'\s*;\s*END\s*;?\s*$/i.test(
|
||
functionBody
|
||
);
|
||
}
|
||
|
||
function hasAllowedAuditLogAppendOnlyFunctionInstall(sqlSource, options = {}) {
|
||
const createStatements = auditLogAppendOnlyFunctionCreateStatements(sqlSource);
|
||
if (createStatements.length !== 1) return false;
|
||
|
||
const definitions = auditLogAppendOnlyFunctionDefinitions(sqlSource);
|
||
if (definitions.length !== 1) return false;
|
||
|
||
const [definition] = definitions;
|
||
if (definition.isCreateOrReplace && !options.allowOrReplace) return false;
|
||
|
||
return isCanonicalAuditLogAppendOnlyFunctionBody(definition.body);
|
||
}
|
||
|
||
function auditLogAppendOnlyFunctionCreateOptions(filePath, content) {
|
||
return {
|
||
// 后续 migration 只允许 plain CREATE FUNCTION 安装固定 append-only guard,不允许替换已有函数。
|
||
allowAuditLogAppendOnlyFunctionPlainCreate:
|
||
isPrismaMigrationSqlPath(filePath) && hasAllowedAuditLogAppendOnlyFunctionInstall(content),
|
||
// 真实 S1 初始 migration 历史上使用 CREATE OR REPLACE;仅该文件按同一函数体语义兼容放行。
|
||
allowAuditLogAppendOnlyFunctionCreateOrReplace:
|
||
filePath === initialAuditLogAppendOnlyFunctionMigration &&
|
||
hasAllowedAuditLogAppendOnlyFunctionInstall(content, { allowOrReplace: true })
|
||
};
|
||
}
|
||
|
||
function isAllowedAuditTriggerAssertion(filePath, content, structure, mutationIndex) {
|
||
if (!auditTriggerAssertionFiles.has(filePath)) return false;
|
||
|
||
let searchCursor = mutationIndex;
|
||
while (searchCursor >= 0) {
|
||
const assertionIndex = structure.lastIndexOf("expectPrismaRejectedInSavepoint", searchCursor);
|
||
if (assertionIndex === -1) return false;
|
||
|
||
const openParenIndex = structure.indexOf("(", assertionIndex);
|
||
if (openParenIndex === -1 || openParenIndex > mutationIndex) {
|
||
searchCursor = assertionIndex - 1;
|
||
continue;
|
||
}
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1 || closeParenIndex < mutationIndex) {
|
||
searchCursor = assertionIndex - 1;
|
||
continue;
|
||
}
|
||
|
||
const assertionSource = content.slice(openParenIndex, closeParenIndex + 1);
|
||
return /AuditLog is append-only/.test(assertionSource);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function hasForbiddenAuditMutation(filePath, content) {
|
||
const structure = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
|
||
for (const match of prismaModelMethodCallMatchesInStructure(content, structure, ["auditLog"], auditPrismaMutationMethods)) {
|
||
if (typeof match.index === "number" && isAllowedAuditTriggerAssertion(filePath, content, structure, match.index)) continue;
|
||
return true;
|
||
}
|
||
|
||
return hasForbiddenRawAuditLogMutation(filePath, content, structure);
|
||
}
|
||
|
||
function guardedStateSqlTableReferencePattern() {
|
||
return String.raw`(?:${guardedStateSqlTables.map(sqlTableReferencePattern).join("|")})`;
|
||
}
|
||
|
||
function hasRawGuardedStateWrite(sqlSource) {
|
||
const sql = normalizeRawSqlScanSource(sqlSource);
|
||
const guardedTable = guardedStateSqlTableReferencePattern();
|
||
const identifier = sqlBareIdentifierPattern();
|
||
|
||
// guarded state 的 raw SQL 必须跟 Prisma raw invocation 同源扫描,覆盖转义字符串、SQL 注释和 Postgres U& identifier。
|
||
return (
|
||
new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(String.raw`\bINSERT\s+INTO\s+(?:ONLY\s+)?${guardedTable}\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(sqlTruncateTableListPattern(guardedTable), "i").test(sql)
|
||
);
|
||
}
|
||
|
||
function hasGuardedPrismaWrite(filePath, content) {
|
||
const hasPrismaWrite = prismaModelMethodCallMatches(content, guardedPrismaWriteModels, guardedPrismaWriteMethods).length > 0;
|
||
const hasRawSqlWrite = filePath.endsWith(".sql")
|
||
? hasRawGuardedStateWrite(content)
|
||
: hasRawSqlInvocationMatching(content, hasRawGuardedStateWrite);
|
||
return hasPrismaWrite || hasRawSqlWrite;
|
||
}
|
||
|
||
function hasRuntimeGuardSetter(content) {
|
||
return /app\.state_transition_guard|set_config\s*\(|SET\s+LOCAL/i.test(content);
|
||
}
|
||
|
||
function isHexDigits(value) {
|
||
return /^[0-9a-f]+$/i.test(value);
|
||
}
|
||
|
||
function decodeJavaScriptStringLiteralBody(body) {
|
||
let output = "";
|
||
|
||
for (let index = 0; index < body.length; index += 1) {
|
||
const char = body[index];
|
||
if (char !== "\\") {
|
||
output += char;
|
||
continue;
|
||
}
|
||
|
||
const next = body[index + 1];
|
||
if (typeof next === "undefined") {
|
||
output += char;
|
||
continue;
|
||
}
|
||
|
||
if (next === "\r" && body[index + 2] === "\n") {
|
||
index += 2;
|
||
continue;
|
||
}
|
||
|
||
if (next === "\n" || next === "\r") {
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
if (next === "x") {
|
||
const hexValue = body.slice(index + 2, index + 4);
|
||
if (hexValue.length === 2 && isHexDigits(hexValue)) {
|
||
output += String.fromCharCode(Number.parseInt(hexValue, 16));
|
||
index += 3;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (next === "u") {
|
||
if (body[index + 2] === "{") {
|
||
const closeBraceIndex = body.indexOf("}", index + 3);
|
||
const hexValue = closeBraceIndex === -1 ? "" : body.slice(index + 3, closeBraceIndex);
|
||
const codePoint = hexValue.length > 0 && isHexDigits(hexValue) ? Number.parseInt(hexValue, 16) : Number.NaN;
|
||
|
||
if (codePoint >= 0 && codePoint <= 0x10ffff) {
|
||
output += String.fromCodePoint(codePoint);
|
||
index = closeBraceIndex;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
const hexValue = body.slice(index + 2, index + 6);
|
||
if (hexValue.length === 4 && isHexDigits(hexValue)) {
|
||
output += String.fromCharCode(Number.parseInt(hexValue, 16));
|
||
index += 5;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
const escapedCharacters = {
|
||
"\\": "\\",
|
||
'"': '"',
|
||
"'": "'",
|
||
"`": "`",
|
||
n: "\n",
|
||
r: "\r",
|
||
t: "\t",
|
||
b: "\b",
|
||
f: "\f",
|
||
v: "\v",
|
||
0: "\0"
|
||
};
|
||
|
||
output += Object.hasOwn(escapedCharacters, next) ? escapedCharacters[next] : next;
|
||
index += 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function decodeStaticJavaScriptStringLiteralSource(source) {
|
||
const literalStart = skipWhitespaceForward(source, 0);
|
||
const literalEndBoundary = skipWhitespaceBackward(source, source.length - 1);
|
||
if (literalStart > literalEndBoundary) return null;
|
||
|
||
const quote = source[literalStart];
|
||
if (quote !== '"' && quote !== "'" && quote !== "`") return null;
|
||
|
||
const literalEnd = findStringLiteralEnd(source, literalStart);
|
||
if (literalEnd !== literalEndBoundary) return null;
|
||
|
||
const body = source.slice(literalStart + 1, literalEnd);
|
||
// 带表达式的 template literal 继续走原有动态 SQL 归一化路径,避免把参数表达式误解成 SQL 文本。
|
||
if (quote === "`" && /\$\{/.test(body)) return null;
|
||
|
||
return decodeJavaScriptStringLiteralBody(body);
|
||
}
|
||
|
||
function decodeStaticJavaScriptStringLiteralsInSource(source) {
|
||
let output = "";
|
||
let index = 0;
|
||
|
||
while (index < source.length) {
|
||
const quote = source[index];
|
||
if (
|
||
quote === '"' &&
|
||
source[index - 1] === "&" &&
|
||
(source[index - 2] === "U" || source[index - 2] === "u")
|
||
) {
|
||
let cursor = index + 1;
|
||
while (cursor < source.length) {
|
||
if (source[cursor] === '"' && source[cursor + 1] === '"') {
|
||
cursor += 2;
|
||
continue;
|
||
}
|
||
cursor += 1;
|
||
if (source[cursor - 1] === '"') break;
|
||
}
|
||
|
||
output += source.slice(index, cursor);
|
||
index = cursor;
|
||
continue;
|
||
}
|
||
|
||
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
||
output += quote;
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
const literalEnd = findStringLiteralEnd(source, index);
|
||
if (literalEnd === -1) {
|
||
output += source.slice(index);
|
||
break;
|
||
}
|
||
|
||
const body = source.slice(index + 1, literalEnd);
|
||
// 动态 template literal 的表达式仍交给 dynamic SQL marker,不能在这里静态展开。
|
||
output += quote === "`" && /\$\{/.test(body) ? source.slice(index, literalEnd + 1) : `${quote}${decodeJavaScriptStringLiteralBody(body)}${quote}`;
|
||
index = literalEnd + 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function sqlDollarQuoteTagAt(content, index) {
|
||
return /^(?:\$\$|\$[A-Za-z_][\w$]*\$)/.exec(content.slice(index))?.[0] ?? null;
|
||
}
|
||
|
||
function decodePostgresUnicodeQuotedIdentifierBody(body, escapeChar = "\\") {
|
||
let output = "";
|
||
|
||
for (let index = 0; index < body.length; index += 1) {
|
||
const char = body[index];
|
||
if (char !== escapeChar) {
|
||
output += char;
|
||
continue;
|
||
}
|
||
|
||
if (body[index + 1] === "+") {
|
||
const hexValue = body.slice(index + 2, index + 8);
|
||
const codePoint = hexValue.length === 6 && isHexDigits(hexValue) ? Number.parseInt(hexValue, 16) : Number.NaN;
|
||
if (codePoint >= 0 && codePoint <= 0x10ffff) {
|
||
output += String.fromCodePoint(codePoint);
|
||
index += 7;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
const hexValue = body.slice(index + 1, index + 5);
|
||
if (hexValue.length === 4 && isHexDigits(hexValue)) {
|
||
output += String.fromCharCode(Number.parseInt(hexValue, 16));
|
||
index += 4;
|
||
continue;
|
||
}
|
||
|
||
output += char;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function postgresUnicodeQuotedIdentifierEscapeClauseAt(content, index) {
|
||
const keywordStart = skipWhitespaceForward(content, index);
|
||
const keywordMatch = /^UESCAPE\b/i.exec(content.slice(keywordStart));
|
||
if (!keywordMatch) return null;
|
||
|
||
const quoteStart = skipWhitespaceForward(content, keywordStart + keywordMatch[0].length);
|
||
if (content[quoteStart] !== "'") return null;
|
||
|
||
const escapeChar = content[quoteStart + 1];
|
||
if (typeof escapeChar === "undefined" || content[quoteStart + 2] !== "'") return null;
|
||
|
||
return { escapeChar, end: quoteStart + 3 };
|
||
}
|
||
|
||
function normalizePostgresUnicodeQuotedIdentifiers(content) {
|
||
let output = "";
|
||
let index = 0;
|
||
|
||
while (index < content.length) {
|
||
if ((content[index] !== "U" && content[index] !== "u") || content[index + 1] !== "&" || content[index + 2] !== '"') {
|
||
output += content[index];
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
let body = "";
|
||
let cursor = index + 3;
|
||
let closeQuoteIndex = -1;
|
||
while (cursor < content.length) {
|
||
const char = content[cursor];
|
||
const next = content[cursor + 1];
|
||
if (char === '"' && next === '"') {
|
||
body += '""';
|
||
cursor += 2;
|
||
continue;
|
||
}
|
||
if (char === '"') {
|
||
closeQuoteIndex = cursor;
|
||
break;
|
||
}
|
||
body += char;
|
||
cursor += 1;
|
||
}
|
||
|
||
if (closeQuoteIndex === -1) {
|
||
output += content[index];
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
const escapeClause = postgresUnicodeQuotedIdentifierEscapeClauseAt(content, closeQuoteIndex + 1);
|
||
// UESCAPE 紧跟 U& 标识符时会改变转义字符;归一化后必须移除该子句,避免污染表名模式。
|
||
output += `"${decodePostgresUnicodeQuotedIdentifierBody(body, escapeClause?.escapeChar ?? "\\")}"`;
|
||
index = escapeClause?.end ?? closeQuoteIndex + 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function stripSqlComments(content) {
|
||
let output = "";
|
||
let index = 0;
|
||
|
||
while (index < content.length) {
|
||
const char = content[index];
|
||
const next = content[index + 1];
|
||
|
||
if (char === "-" && next === "-") {
|
||
// 真实行注释替换为空白但保留换行,避免擦掉下一条 SQL 语句的行边界。
|
||
while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
|
||
output += " ";
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (char === "/" && next === "*") {
|
||
// 注释可能夹在关键字之间,必须保留 token separator,避免 DELETE/*x*/FROM 被拼成 DELETEFROM。
|
||
let depth = 1;
|
||
output += " ";
|
||
index += 2;
|
||
|
||
while (index < content.length && depth > 0) {
|
||
const blockChar = content[index];
|
||
const blockNext = content[index + 1];
|
||
if (blockChar === "/" && blockNext === "*") {
|
||
output += " ";
|
||
index += 2;
|
||
depth += 1;
|
||
continue;
|
||
}
|
||
|
||
if (blockChar === "*" && blockNext === "/") {
|
||
output += " ";
|
||
index += 2;
|
||
depth -= 1;
|
||
continue;
|
||
}
|
||
|
||
output += blockChar === "\n" || blockChar === "\r" ? blockChar : " ";
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (char === "'") {
|
||
output += char;
|
||
index += 1;
|
||
|
||
while (index < content.length) {
|
||
const stringChar = content[index];
|
||
const stringNext = content[index + 1];
|
||
output += stringChar;
|
||
|
||
if (stringChar === "\\") {
|
||
if (typeof stringNext !== "undefined") output += stringNext;
|
||
index += typeof stringNext === "undefined" ? 1 : 2;
|
||
continue;
|
||
}
|
||
|
||
if (stringChar === "'" && stringNext === "'") {
|
||
output += stringNext;
|
||
index += 2;
|
||
continue;
|
||
}
|
||
|
||
index += 1;
|
||
if (stringChar === "'") break;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (char === '"') {
|
||
output += char;
|
||
index += 1;
|
||
|
||
while (index < content.length) {
|
||
const identifierChar = content[index];
|
||
const identifierNext = content[index + 1];
|
||
output += identifierChar;
|
||
|
||
if (identifierChar === "\\") {
|
||
if (typeof identifierNext !== "undefined") output += identifierNext;
|
||
index += typeof identifierNext === "undefined" ? 1 : 2;
|
||
continue;
|
||
}
|
||
|
||
if (identifierChar === '"' && identifierNext === '"') {
|
||
output += identifierNext;
|
||
index += 2;
|
||
continue;
|
||
}
|
||
|
||
index += 1;
|
||
if (identifierChar === '"') break;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
const dollarQuoteTag = char === "$" ? sqlDollarQuoteTagAt(content, index) : null;
|
||
if (dollarQuoteTag) {
|
||
const dollarQuoteEnd = content.indexOf(dollarQuoteTag, index + dollarQuoteTag.length);
|
||
if (dollarQuoteEnd === -1) {
|
||
output += content.slice(index);
|
||
break;
|
||
}
|
||
|
||
output += content.slice(index, dollarQuoteEnd + dollarQuoteTag.length);
|
||
index = dollarQuoteEnd + dollarQuoteTag.length;
|
||
continue;
|
||
}
|
||
|
||
output += char;
|
||
index += 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function normalizeRawSqlScanSource(content) {
|
||
const sqlSource = decodeStaticJavaScriptStringLiteralSource(content) ?? decodeStaticJavaScriptStringLiteralsInSource(content);
|
||
|
||
return normalizePostgresUnicodeQuotedIdentifiers(stripSqlComments(sqlSource))
|
||
.replace(/\\(["'`])/g, "$1")
|
||
.replace(/\\n/g, "\n")
|
||
.replace(/\\r/g, "\r")
|
||
.replace(/\\t/g, "\t")
|
||
.replace(
|
||
/\$\{\s*Prisma\s*\.\s*raw\s*\(\s*((?:"(?:\\.|[^"\\])*")|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)\s*\)\s*\}/gs,
|
||
(_match, literalSource) => {
|
||
// Prisma.raw(...) 的静态 identifier 片段也必须按 JS 运行时字符串解码后再进入 SQL 归一化。
|
||
return ` ${decodeStaticJavaScriptStringLiteralSource(literalSource) ?? literalSource} `;
|
||
}
|
||
)
|
||
.replace(/\$\{[\s\S]*?\}/g, " ? ")
|
||
.replace(/["'`]/g, " ")
|
||
.replace(/\s*\+\s*/g, " ")
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
function sqlTableReferencePattern(tableName) {
|
||
const identifier = sqlBareIdentifierPattern();
|
||
return String.raw`(?:${identifier}\s*\.\s*)?${escapeRegExp(tableName)}`;
|
||
}
|
||
|
||
function sqlAnyTableReferencePattern() {
|
||
const identifier = sqlBareIdentifierPattern();
|
||
return String.raw`(?:${identifier}\s*\.\s*)?${identifier}`;
|
||
}
|
||
|
||
function sqlFunctionReferencePattern(functionName) {
|
||
const identifier = sqlBareIdentifierPattern();
|
||
return String.raw`(?:${identifier}\s*\.\s*)?${escapeRegExp(functionName)}\s*(?:\([^)]*\))?`;
|
||
}
|
||
|
||
function sqlSpecificFunctionReferencePattern(functionName) {
|
||
const identifier = sqlBareIdentifierPattern();
|
||
return String.raw`(?:${identifier}\s*\.\s*)?${escapeRegExp(functionName)}\b\s*(?:\([^)]*\))?`;
|
||
}
|
||
|
||
function sqlAnyFunctionReferencePattern() {
|
||
const identifier = sqlBareIdentifierPattern();
|
||
return String.raw`(?:${identifier}\s*\.\s*)?${identifier}\s*(?:\([^)]*\))?`;
|
||
}
|
||
|
||
function sqlFunctionListContainingPattern(targetFunctionReference) {
|
||
const functionReference = sqlAnyFunctionReferencePattern();
|
||
return String.raw`(?:${functionReference}\s*,\s*)*${targetFunctionReference}(?:\s*,\s*${functionReference})*`;
|
||
}
|
||
|
||
function staticStringLiteralAlternatives(values) {
|
||
return values.flatMap((value) => {
|
||
const escapedValue = escapeRegExp(value);
|
||
return [`"${escapedValue}"`, `'${escapedValue}'`, `\`${escapedValue}\``];
|
||
});
|
||
}
|
||
|
||
function staticPrismaPropertyAccessPattern(values) {
|
||
const dotNames = values.map(escapeRegExp).join("|");
|
||
const bracketNames = staticStringLiteralAlternatives(values).join("|");
|
||
return String.raw`(?:(?:\.|\?\.)\s*(?:${dotNames})|(?:\?\.\s*)?\[\s*(?:${bracketNames})\s*\])`;
|
||
}
|
||
|
||
function staticPrismaPropertyAccessMatchAt(content, structure, index, values, beforeIndex = index) {
|
||
const allowedValues = new Set(values);
|
||
let propertyStart = index;
|
||
|
||
if (structure.startsWith("?.", index)) {
|
||
propertyStart = skipWhitespaceForward(structure, index + 2);
|
||
} else if (structure[index] === ".") {
|
||
propertyStart = skipWhitespaceForward(structure, index + 1);
|
||
}
|
||
|
||
if (structure[propertyStart] === "[") {
|
||
const closeBracketIndex = findMatchingBracket(structure, propertyStart);
|
||
if (closeBracketIndex === -1) return null;
|
||
|
||
const expressionStart = propertyStart + 1;
|
||
const expressionEnd = closeBracketIndex;
|
||
const resolvedValues = staticJavaScriptExpressionValues(
|
||
content,
|
||
structure,
|
||
content.slice(expressionStart, expressionEnd),
|
||
expressionStart,
|
||
beforeIndex,
|
||
new Set(),
|
||
0
|
||
);
|
||
const value = resolvedValues.find((candidate) => allowedValues.has(candidate));
|
||
return value ? { index, end: closeBracketIndex + 1, value } : null;
|
||
}
|
||
|
||
if (propertyStart === index && structure[index] !== "." && !structure.startsWith("?.", index)) return null;
|
||
|
||
const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(propertyStart));
|
||
if (!nameMatch || !allowedValues.has(nameMatch[0])) return null;
|
||
|
||
return { index, end: propertyStart + nameMatch[0].length, value: nameMatch[0] };
|
||
}
|
||
|
||
function staticPrismaPropertyAccessMatches(content, structure, values, startIndex = 0, endIndex = structure.length) {
|
||
const matches = [];
|
||
|
||
for (let cursor = startIndex; cursor < endIndex; cursor += 1) {
|
||
if (structure[cursor] !== "." && structure[cursor] !== "[" && !structure.startsWith("?.", cursor)) continue;
|
||
|
||
const match = staticPrismaPropertyAccessMatchAt(content, structure, cursor, values, cursor);
|
||
if (!match || match.end > endIndex) continue;
|
||
|
||
// computed key 统一走静态 JS 表达式解析,覆盖多段拼接、变量 alias 和完整括号表达式。
|
||
matches.push(match);
|
||
cursor = match.end - 1;
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
function staticPrismaDelegateExpressionMatchAt(content, structure, index, models) {
|
||
const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(index));
|
||
if (!receiverMatch) return null;
|
||
|
||
const modelAccessStart = skipWhitespaceForward(structure, index + receiverMatch[0].length);
|
||
const modelAccess = staticPrismaPropertyAccessMatchAt(content, structure, modelAccessStart, models, modelAccessStart);
|
||
return modelAccess ? { index, end: modelAccess.end, model: modelAccess.value } : null;
|
||
}
|
||
|
||
function hasOptionalCallOperatorAt(structure, index) {
|
||
const callStart = skipWhitespaceForward(structure, index);
|
||
if (structure.startsWith("?.", callStart)) {
|
||
return skipWhitespaceForward(structure, callStart + 2);
|
||
}
|
||
return callStart;
|
||
}
|
||
|
||
function functionPrototypeMethodKind(structure, sourceStart, sourceEnd) {
|
||
const start = skipWhitespaceForward(structure, sourceStart);
|
||
const end = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
if (start >= end) return null;
|
||
|
||
const normalizedSource = structure.slice(start, end).replace(/\s+/g, "");
|
||
if (normalizedSource === "Function.prototype.call") return "call";
|
||
if (normalizedSource === "Function.prototype.apply") return "apply";
|
||
return null;
|
||
}
|
||
|
||
function staticBuiltinInvokerKind(structure, sourceStart, sourceEnd) {
|
||
const functionPrototypeKind = functionPrototypeMethodKind(structure, sourceStart, sourceEnd);
|
||
if (functionPrototypeKind) return functionPrototypeKind;
|
||
|
||
const start = skipWhitespaceForward(structure, sourceStart);
|
||
const end = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
if (start >= end) return null;
|
||
|
||
const normalizedSource = structure.slice(start, end).replace(/\s+/g, "");
|
||
return normalizedSource === "Reflect.apply" ? "reflectApply" : null;
|
||
}
|
||
|
||
function staticBuiltinInvokerBaseExpressionMatchAt(structure, index) {
|
||
return staticBuiltinInvokerBaseExpressionMatchAtWithContent("", structure, index);
|
||
}
|
||
|
||
function staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, index) {
|
||
const expressionStart = skipWhitespaceForward(structure, index);
|
||
const rootMatch = /^(?:Function|Reflect)\b/.exec(structure.slice(expressionStart));
|
||
if (!rootMatch) return null;
|
||
|
||
const firstAccessStart = skipWhitespaceForward(structure, expressionStart + rootMatch[0].length);
|
||
const firstAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, firstAccessStart);
|
||
if (!firstAccess) return null;
|
||
|
||
if (rootMatch[0] === "Reflect") {
|
||
return firstAccess.value === "apply" ? { kind: "reflectApply", end: firstAccess.end } : null;
|
||
}
|
||
|
||
if (firstAccess.value !== "prototype") return null;
|
||
|
||
const secondAccessStart = skipWhitespaceForward(structure, firstAccess.end);
|
||
const secondAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, secondAccessStart);
|
||
if (!secondAccess || (secondAccess.value !== "call" && secondAccess.value !== "apply")) return null;
|
||
|
||
return { kind: secondAccess.value, end: secondAccess.end };
|
||
}
|
||
|
||
function staticMemberOperationMatchAt(content, structure, index, values) {
|
||
const accessStart = skipWhitespaceForward(structure, index);
|
||
const access = staticStaticMemberPropertyAccessMatchAt(content, structure, accessStart);
|
||
if (!access || !values.includes(access.value)) return null;
|
||
|
||
return { value: access.value, end: access.end };
|
||
}
|
||
|
||
function staticBuiltinMemberOperationMatchAt(content, structure, index, values) {
|
||
return staticMemberOperationMatchAt(content, structure, index, values);
|
||
}
|
||
|
||
function staticCallableMemberOperationMatchAt(content, structure, index, values) {
|
||
const operation = staticMemberOperationMatchAt(content, structure, index, values);
|
||
if (!operation) return null;
|
||
|
||
const openParenIndex = skipOptionalTypeArguments(structure, operation.end);
|
||
if (structure[openParenIndex] !== "(") return null;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) return null;
|
||
|
||
return { kind: operation.value, methodNameEnd: operation.end, openParenIndex, closeParenIndex };
|
||
}
|
||
|
||
function isStaticBuiltinInvokerBindReceiver(content, structure, kind, receiverArgument) {
|
||
if (!receiverArgument) return false;
|
||
|
||
const receiverStart = skipWhitespaceForward(structure, receiverArgument.index);
|
||
const receiverEnd = skipWhitespaceBackward(structure, receiverArgument.end - 1) + 1;
|
||
if (receiverStart >= receiverEnd) return false;
|
||
|
||
if (kind === "reflectApply") {
|
||
// Reflect.apply 的 this 不参与语义,`Reflect.apply.bind(null|undefined|Reflect)` 都等价;只要 target 是静态 Reflect.apply 就登记 alias。
|
||
return true;
|
||
}
|
||
|
||
const receiverMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, receiverStart);
|
||
return receiverMatch?.kind === kind && skipWhitespaceForward(structure, receiverMatch.end) === receiverEnd;
|
||
}
|
||
|
||
function staticBuiltinInvokerExpressionMatchAt(content, structure, index) {
|
||
const expressionStart = skipWhitespaceForward(structure, index);
|
||
const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, expressionStart);
|
||
if (!baseMatch) return null;
|
||
|
||
const bindMethodStart = skipWhitespaceForward(structure, baseMatch.end);
|
||
const bindMethodMatch = staticBuiltinMemberOperationMatchAt(content, structure, bindMethodStart, ["bind"]);
|
||
if (!bindMethodMatch) return { kind: baseMatch.kind, end: baseMatch.end, bound: false };
|
||
|
||
const bindCallStart = skipWhitespaceForward(structure, bindMethodMatch.end);
|
||
if (structure[bindCallStart] !== "(") return null;
|
||
|
||
const bindCallEnd = findMatchingParen(structure, bindCallStart);
|
||
if (bindCallEnd === -1) return null;
|
||
|
||
const receiverArgument = nthCallArgumentSource(content, structure, bindCallStart, bindCallEnd, 0);
|
||
if (!isStaticBuiltinInvokerBindReceiver(content, structure, baseMatch.kind, receiverArgument)) return null;
|
||
|
||
return { kind: baseMatch.kind, end: bindCallEnd + 1, bound: true };
|
||
}
|
||
|
||
function findStaticBuiltinInvokerAliases(content, structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const invokerMatch = staticBuiltinInvokerExpressionMatchAt(content, structure, initializerStart);
|
||
if (!invokerMatch) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, invokerMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 只接受同文件静态内建调用器 alias,复杂表达式不进入 scope gate 语义展开。
|
||
aliases.push({
|
||
name: declarationMatch[1],
|
||
kind: invokerMatch.kind,
|
||
bound: invokerMatch.bound,
|
||
index: declarationMatch.index,
|
||
end: aliasEnd
|
||
});
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const invokerMatch = staticBuiltinInvokerExpressionMatchAt(content, structure, initializerStart);
|
||
if (!invokerMatch) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, invokerMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 赋值 alias 覆盖 `let c; c = Function.prototype.call` 这类后续绑定。
|
||
aliases.push({ name: variableName, kind: invokerMatch.kind, bound: invokerMatch.bound, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
return propagateStaticIdentifierAliases(structure, aliases, (sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
kind: sourceAlias.kind,
|
||
bound: sourceAlias.bound,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
}
|
||
|
||
function hasSimpleIdentifierAssignmentBetween(structure, variableName, startIndex, beforeIndex) {
|
||
const assignmentPattern = new RegExp(String.raw`\b${escapeRegExp(variableName)}\b\s*=`, "g");
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number" || assignmentMatch.index <= startIndex || assignmentMatch.index >= beforeIndex) continue;
|
||
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1 || equalsIndex >= beforeIndex) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex) {
|
||
const latestAlias = aliases
|
||
.filter((alias) => alias.name === aliasName && (alias.end ?? alias.index) < beforeIndex)
|
||
.toSorted((left, right) => (right.end ?? right.index) - (left.end ?? left.index))[0];
|
||
|
||
if (!latestAlias) return null;
|
||
if (hasSimpleIdentifierAssignmentBetween(structure, aliasName, latestAlias.end ?? latestAlias.index, beforeIndex)) return null;
|
||
|
||
return latestAlias;
|
||
}
|
||
|
||
function simpleIdentifierAliasInitializers(structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const initializerMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(initializerStart));
|
||
if (!initializerMatch) continue;
|
||
|
||
const initializerEnd = initializerStart + initializerMatch[0].length;
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, initializerEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({
|
||
name: declarationMatch[1],
|
||
initializerName: initializerMatch[0],
|
||
index: declarationMatch.index,
|
||
initializerStart,
|
||
end: aliasEnd
|
||
});
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const initializerMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(initializerStart));
|
||
if (!initializerMatch) continue;
|
||
|
||
const initializerEnd = initializerStart + initializerMatch[0].length;
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, initializerEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({
|
||
name: variableName,
|
||
initializerName: initializerMatch[0],
|
||
index: assignmentMatch.index,
|
||
initializerStart,
|
||
end: aliasEnd
|
||
});
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function propagateStaticIdentifierAliases(structure, aliases, copyAlias) {
|
||
const propagated = [...aliases];
|
||
const initializers = simpleIdentifierAliasInitializers(structure);
|
||
|
||
for (let depth = 0; depth < rawSqlVariableResolutionMaxDepth; depth += 1) {
|
||
let changed = false;
|
||
|
||
for (const initializer of initializers) {
|
||
if (initializer.name === initializer.initializerName) continue;
|
||
if (propagated.some((alias) => alias.name === initializer.name && (alias.end ?? alias.index) === initializer.end)) continue;
|
||
|
||
const sourceAlias = latestStaticAliasRecordAt(structure, propagated, initializer.initializerName, initializer.initializerStart);
|
||
if (!sourceAlias) continue;
|
||
|
||
propagated.push(copyAlias(sourceAlias, initializer));
|
||
changed = true;
|
||
}
|
||
|
||
if (!changed) break;
|
||
}
|
||
|
||
return propagated.toSorted((left, right) => (left.end ?? left.index) - (right.end ?? right.index));
|
||
}
|
||
|
||
function completeStaticWrapperSourceMatch(content, structure, sourceMatcher, sourceStart, sourceEnd, beforeIndex) {
|
||
const expressionStart = skipWhitespaceForward(structure, sourceStart);
|
||
const expressionEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
const match = sourceMatcher(content, structure, expressionStart, expressionEnd, beforeIndex);
|
||
if (!match) return null;
|
||
|
||
return isAtStaticWrapperExpressionEnd(structure, match.end, expressionEnd) ? match : null;
|
||
}
|
||
|
||
function staticIdentifierAliasSourceMatchAt(structure, aliases, sourceStart, sourceEnd, beforeIndex) {
|
||
const aliasName = simpleIdentifierSourceName(structure, sourceStart, sourceEnd);
|
||
if (!aliasName) return null;
|
||
|
||
const alias = latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex);
|
||
return alias ? { index: sourceStart, end: sourceEnd, alias } : null;
|
||
}
|
||
|
||
function isAtStaticWrapperExpressionEnd(structure, cursor, expressionEnd) {
|
||
// wrapper source 已按表达式边界 trim;结束判断不能继续越过 `}`、`]` 或 `;`。
|
||
return cursor === expressionEnd || (cursor < expressionEnd && skipWhitespaceForward(structure, cursor) === expressionEnd);
|
||
}
|
||
|
||
function staticWrapperSourcesInExpression(content, structure, sourceMatcher, expressionStart, expressionEnd, beforeIndex, basePath, depth) {
|
||
if (depth >= rawSqlVariableResolutionMaxDepth) return [];
|
||
|
||
const wrappers = [];
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, expressionStart, expressionEnd);
|
||
const valueStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const valueEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (valueStart >= valueEnd) return wrappers;
|
||
|
||
const sourceMatch = completeStaticWrapperSourceMatch(content, structure, sourceMatcher, valueStart, valueEnd, beforeIndex);
|
||
if (sourceMatch) {
|
||
wrappers.push({ path: basePath, source: sourceMatch });
|
||
return wrappers;
|
||
}
|
||
|
||
const objectAssignSources = staticObjectAssignArgumentSources(content, structure, valueStart, valueEnd);
|
||
if (objectAssignSources.length > 0) {
|
||
for (const objectAssignSource of objectAssignSources) {
|
||
wrappers.push(
|
||
...staticWrapperSourcesInExpression(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
objectAssignSource.index,
|
||
objectAssignSource.end,
|
||
beforeIndex,
|
||
basePath,
|
||
depth + 1
|
||
)
|
||
);
|
||
}
|
||
return wrappers;
|
||
}
|
||
|
||
if (structure[valueStart] === "{") {
|
||
const objectEnd = findMatchingBrace(structure, valueStart);
|
||
if (objectEnd === -1 || objectEnd !== valueEnd - 1) return wrappers;
|
||
|
||
for (const part of destructuringParts(structure, valueStart + 1, objectEnd)) {
|
||
const propertyName = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex);
|
||
if (!propertyName) continue;
|
||
|
||
const keyStart = skipWhitespaceForward(structure, part.start);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, part.end);
|
||
if (keyEnd === -1) continue;
|
||
|
||
const colonIndex = skipWhitespaceForward(structure, keyEnd);
|
||
if (structure[colonIndex] !== ":") {
|
||
const shorthandMatch = completeStaticWrapperSourceMatch(content, structure, sourceMatcher, keyStart, keyEnd, beforeIndex);
|
||
if (shorthandMatch) wrappers.push({ path: basePath.concat(propertyName), source: shorthandMatch });
|
||
continue;
|
||
}
|
||
|
||
wrappers.push(
|
||
...staticWrapperSourcesInExpression(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
skipWhitespaceForward(structure, colonIndex + 1),
|
||
part.end,
|
||
beforeIndex,
|
||
basePath.concat(propertyName),
|
||
depth + 1
|
||
)
|
||
);
|
||
}
|
||
return wrappers;
|
||
}
|
||
|
||
if (structure[valueStart] === "[") {
|
||
const arrayEnd = findMatchingBracket(structure, valueStart);
|
||
if (arrayEnd === -1 || arrayEnd !== valueEnd - 1) return wrappers;
|
||
|
||
const parts = destructuringParts(structure, valueStart + 1, arrayEnd);
|
||
for (const [elementIndex, part] of parts.entries()) {
|
||
wrappers.push(
|
||
...staticWrapperSourcesInExpression(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
part.start,
|
||
part.end,
|
||
beforeIndex,
|
||
basePath.concat(elementIndex),
|
||
depth + 1
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
return wrappers;
|
||
}
|
||
|
||
function staticObjectAssignArgumentSources(content, structure, sourceStart, sourceEnd) {
|
||
if (!structure.startsWith("Object", sourceStart)) return [];
|
||
|
||
const assignAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, sourceStart + "Object".length);
|
||
if (!assignAccess || assignAccess.value !== "assign") return [];
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, assignAccess.end);
|
||
if (structure[openParenIndex] !== "(") return [];
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1 || skipWhitespaceForward(structure, closeParenIndex + 1) !== sourceEnd) return [];
|
||
|
||
const sources = [];
|
||
let argumentIndex = 1;
|
||
while (true) {
|
||
const argument = nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, argumentIndex);
|
||
if (!argument) break;
|
||
|
||
sources.push(argument);
|
||
argumentIndex += 1;
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function staticObjectWrapperSources(content, structure, sourceMatcher) {
|
||
const wrappers = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const expressionEnd = skipWhitespaceBackward(structure, statementEnd - 1) + 1;
|
||
|
||
for (const wrapper of staticWrapperSourcesInExpression(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
initializerStart,
|
||
expressionEnd,
|
||
declarationMatch.index,
|
||
[],
|
||
0
|
||
)) {
|
||
if (wrapper.path.length === 0) continue;
|
||
wrappers.push({
|
||
name: declarationMatch[1],
|
||
path: wrapper.path,
|
||
kind: "path",
|
||
index: declarationMatch.index,
|
||
end: statementEnd,
|
||
source: wrapper.source
|
||
});
|
||
}
|
||
}
|
||
|
||
return wrappers;
|
||
}
|
||
|
||
function staticArrayWrapperSources(_content, _structure, _sourceMatcher) {
|
||
// 数组 wrapper 已由 staticObjectWrapperSources 的 path-source 递归统一覆盖。
|
||
return [];
|
||
}
|
||
|
||
function staticFunctionReturnWrapperSources(content, structure, sourceMatcher) {
|
||
const wrappers = [];
|
||
const functionPattern = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g;
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
|
||
for (const functionMatch of structure.matchAll(functionPattern)) {
|
||
if (typeof functionMatch.index !== "number") continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", functionMatch.index);
|
||
const closeBraceIndex = openBraceIndex === -1 ? -1 : findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
|
||
const bodyStart = openBraceIndex + 1;
|
||
const bodyEnd = closeBraceIndex;
|
||
const returnPattern = /\breturn\b/g;
|
||
returnPattern.lastIndex = bodyStart;
|
||
const returnMatch = returnPattern.exec(structure);
|
||
if (!returnMatch || returnMatch.index >= bodyEnd) continue;
|
||
|
||
const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length);
|
||
const statementEnd = Math.min(findStatementEnd(structure, expressionStart), bodyEnd);
|
||
wrappers.push(
|
||
...staticFunctionReturnExpressionWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
functionMatch[1],
|
||
functionMatch.index,
|
||
closeBraceIndex + 1,
|
||
expressionStart,
|
||
statementEnd
|
||
)
|
||
);
|
||
}
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const functionExpressionMatch = /^function\b\s*(?:[A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{/.exec(structure.slice(initializerStart));
|
||
if (functionExpressionMatch) {
|
||
const openBraceIndex = structure.indexOf("{", initializerStart + functionExpressionMatch[0].length - 1);
|
||
const closeBraceIndex = openBraceIndex === -1 ? -1 : findMatchingBrace(structure, openBraceIndex);
|
||
const statementEnd = closeBraceIndex === -1 ? -1 : findStatementEnd(structure, closeBraceIndex + 1);
|
||
if (closeBraceIndex !== -1 && statementEnd !== -1) {
|
||
const returnPattern = /\breturn\b/g;
|
||
returnPattern.lastIndex = openBraceIndex + 1;
|
||
const returnMatch = returnPattern.exec(structure);
|
||
if (returnMatch && returnMatch.index < closeBraceIndex) {
|
||
const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length);
|
||
const expressionEnd = Math.min(findStatementEnd(structure, expressionStart), closeBraceIndex);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeBraceIndex + 1, statementEnd);
|
||
if (aliasEnd !== -1) {
|
||
wrappers.push(
|
||
...staticFunctionReturnExpressionWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
declarationMatch[1],
|
||
declarationMatch.index,
|
||
aliasEnd,
|
||
expressionStart,
|
||
expressionEnd
|
||
)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
let statementEnd = findStatementEnd(structure, initializerStart);
|
||
const afterArrow = staticArrowExpressionBodyStart(structure, initializerStart, statementEnd);
|
||
if (afterArrow === -1) continue;
|
||
if (structure[afterArrow] === "{") {
|
||
const closeBraceIndex = findMatchingBrace(structure, afterArrow);
|
||
if (closeBraceIndex !== -1) statementEnd = findStatementEnd(structure, closeBraceIndex + 1);
|
||
const aliasEnd = closeBraceIndex === -1 ? -1 : simpleStaticAliasInitializerEnd(structure, closeBraceIndex + 1, statementEnd);
|
||
if (closeBraceIndex === -1 || aliasEnd === -1) continue;
|
||
|
||
wrappers.push(
|
||
...staticBlockReturnWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
declarationMatch[1],
|
||
declarationMatch.index,
|
||
aliasEnd,
|
||
afterArrow
|
||
)
|
||
);
|
||
continue;
|
||
}
|
||
|
||
const expressionEnd = skipWhitespaceBackward(structure, statementEnd - 1) + 1;
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
wrappers.push(
|
||
...staticFunctionReturnExpressionWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
declarationMatch[1],
|
||
declarationMatch.index,
|
||
aliasEnd,
|
||
afterArrow,
|
||
expressionEnd
|
||
)
|
||
);
|
||
}
|
||
|
||
return wrappers;
|
||
}
|
||
|
||
function staticBlockReturnWrapperSources(content, structure, sourceMatcher, functionName, functionIndex, functionEnd, openBraceIndex) {
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) return [];
|
||
|
||
const bodyStart = openBraceIndex + 1;
|
||
const bodyEnd = closeBraceIndex;
|
||
const returnPattern = /\breturn\b/g;
|
||
returnPattern.lastIndex = bodyStart;
|
||
const returnMatch = returnPattern.exec(structure);
|
||
if (!returnMatch || returnMatch.index >= bodyEnd) return [];
|
||
|
||
const nextReturn = returnPattern.exec(structure);
|
||
if (nextReturn && nextReturn.index < bodyEnd) return [];
|
||
|
||
const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length);
|
||
const statementEnd = Math.min(findStatementEnd(structure, expressionStart), bodyEnd);
|
||
return staticFunctionReturnExpressionWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
functionName,
|
||
functionIndex,
|
||
functionEnd,
|
||
expressionStart,
|
||
statementEnd
|
||
);
|
||
}
|
||
|
||
function staticArrowExpressionBodyStart(structure, initializerStart, statementEnd) {
|
||
const arrowStart = skipWhitespaceForward(structure, initializerStart);
|
||
let cursor = arrowStart;
|
||
|
||
if (structure[cursor] === "(") {
|
||
const paramsEnd = findMatchingParen(structure, cursor);
|
||
if (paramsEnd === -1 || paramsEnd >= statementEnd) return -1;
|
||
cursor = skipWhitespaceForward(structure, paramsEnd + 1);
|
||
} else {
|
||
const identifierMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(cursor));
|
||
if (!identifierMatch) return -1;
|
||
cursor = skipWhitespaceForward(structure, cursor + identifierMatch[0].length);
|
||
}
|
||
|
||
if (structure[cursor] === ":") {
|
||
cursor = staticTypeAnnotationArrowIndex(structure, cursor + 1, statementEnd);
|
||
if (cursor === -1) return -1;
|
||
}
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor);
|
||
if (!structure.startsWith("=>", cursor)) return -1;
|
||
return skipWhitespaceForward(structure, cursor + 2);
|
||
}
|
||
|
||
function staticTypeAnnotationArrowIndex(structure, sourceStart, sourceEnd) {
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
|
||
for (let cursor = sourceStart; cursor < sourceEnd - 1; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
||
|
||
if (structure.startsWith("=>", cursor) && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
||
return cursor;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
function staticFunctionReturnExpressionWrapperSources(
|
||
content,
|
||
structure,
|
||
sourceMatcher,
|
||
functionName,
|
||
functionIndex,
|
||
functionEnd,
|
||
expressionStart,
|
||
expressionEnd
|
||
) {
|
||
// 函数返回 object/array 时保留内部静态 path,供 getBox().raw 与 const { raw } = getBox() 共用。
|
||
return staticWrapperSourcesInExpression(content, structure, sourceMatcher, expressionStart, expressionEnd, functionIndex, [], 0).map(
|
||
(wrapper) => ({
|
||
kind: wrapper.path.length === 0 ? "functionReturn" : "functionReturnPath",
|
||
name: functionName,
|
||
path: wrapper.path,
|
||
index: functionIndex,
|
||
end: functionEnd,
|
||
source: wrapper.source
|
||
})
|
||
);
|
||
}
|
||
|
||
function staticWrapperSources(content, structure, sourceMatcher) {
|
||
return staticObjectWrapperSources(content, structure, sourceMatcher)
|
||
.concat(staticArrayWrapperSources(content, structure, sourceMatcher))
|
||
.concat(staticFunctionReturnWrapperSources(content, structure, sourceMatcher))
|
||
.toSorted((left, right) => left.end - right.end);
|
||
}
|
||
|
||
function isStaticWrapperActiveAt(structure, wrapper, beforeIndex) {
|
||
if (wrapper.end >= beforeIndex) return false;
|
||
return !hasSimpleIdentifierAssignmentBetween(structure, wrapper.name, wrapper.end, beforeIndex);
|
||
}
|
||
|
||
function staticArrayIndexAccessMatchAt(structure, index, expectedIndex) {
|
||
let bracketStart = skipWhitespaceForward(structure, index);
|
||
if (structure.startsWith("?.", bracketStart)) {
|
||
bracketStart = skipWhitespaceForward(structure, bracketStart + 2);
|
||
}
|
||
if (structure[bracketStart] !== "[") return null;
|
||
|
||
const bracketEnd = findMatchingBracket(structure, bracketStart);
|
||
if (bracketEnd === -1) return null;
|
||
|
||
const indexSource = structure.slice(bracketStart + 1, bracketEnd).trim();
|
||
if (!/^\d+$/.test(indexSource) || Number.parseInt(indexSource, 10) !== expectedIndex) return null;
|
||
|
||
return { index, end: bracketEnd + 1 };
|
||
}
|
||
|
||
function staticWrapperPathAccessMatchAt(content, structure, index, path) {
|
||
let cursor = index;
|
||
|
||
for (const segment of path) {
|
||
const accessStart = skipWhitespaceForward(structure, cursor);
|
||
if (typeof segment === "number") {
|
||
const arrayAccess = staticArrayIndexAccessMatchAt(structure, accessStart, segment);
|
||
if (!arrayAccess) return null;
|
||
cursor = arrayAccess.end;
|
||
continue;
|
||
}
|
||
|
||
const propertyAccess = staticPrismaPropertyAccessMatchAt(content, structure, accessStart, [segment], accessStart);
|
||
if (!propertyAccess) return null;
|
||
cursor = propertyAccess.end;
|
||
}
|
||
|
||
return { index, end: cursor };
|
||
}
|
||
|
||
function staticWrapperSourceUsageMatchAt(content, structure, wrappers, index, beforeIndex = index) {
|
||
const usageStart = skipWhitespaceForward(structure, index);
|
||
const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(usageStart));
|
||
if (!nameMatch || isMemberIdentifierReference(structure, usageStart)) return null;
|
||
|
||
const name = nameMatch[0];
|
||
const afterName = skipWhitespaceForward(structure, usageStart + name.length);
|
||
const candidates = wrappers.filter((wrapper) => wrapper.name === name && isStaticWrapperActiveAt(structure, wrapper, beforeIndex));
|
||
|
||
for (const wrapper of candidates.toSorted((left, right) => right.end - left.end)) {
|
||
if (wrapper.kind === "path") {
|
||
const pathAccess = staticWrapperPathAccessMatchAt(content, structure, afterName, wrapper.path);
|
||
if (pathAccess) return { index: usageStart, end: pathAccess.end, wrapper };
|
||
continue;
|
||
}
|
||
|
||
if (wrapper.kind === "functionReturn" || wrapper.kind === "functionReturnPath") {
|
||
const callEnd = staticZeroArgumentCallEndAt(content, structure, afterName);
|
||
if (callEnd === -1) continue;
|
||
|
||
if (wrapper.kind === "functionReturn") return { index: usageStart, end: callEnd, wrapper };
|
||
|
||
const pathAccess = staticWrapperPathAccessMatchAt(content, structure, callEnd, wrapper.path);
|
||
if (pathAccess) return { index: usageStart, end: pathAccess.end, wrapper };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function staticZeroArgumentCallEndAt(content, structure, index) {
|
||
let openParenIndex = skipWhitespaceForward(structure, index);
|
||
if (structure.startsWith("?.", openParenIndex)) {
|
||
openParenIndex = skipWhitespaceForward(structure, openParenIndex + 2);
|
||
}
|
||
if (structure[openParenIndex] !== "(") return -1;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) return -1;
|
||
|
||
return nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, 0) ? -1 : closeParenIndex + 1;
|
||
}
|
||
|
||
function staticWrapperSourceUsageMatches(content, structure, wrappers) {
|
||
const matches = [];
|
||
const names = [...new Set(wrappers.map((wrapper) => wrapper.name))];
|
||
if (names.length === 0) return matches;
|
||
|
||
const namePattern = new RegExp(String.raw`\b(${names.map(escapeRegExp).join("|")})\b`, "g");
|
||
for (const match of structure.matchAll(namePattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, match.index, match.index);
|
||
if (usage) matches.push(usage);
|
||
}
|
||
|
||
return matches.toSorted((left, right) => left.index - right.index);
|
||
}
|
||
|
||
function findStaticWrapperUsageAliases(content, structure, wrappers) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, initializerStart, declarationMatch.index);
|
||
if (!usage) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, usage.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, initializerStart, assignmentMatch.index);
|
||
if (!usage) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, usage.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function pathsEqual(left, right) {
|
||
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||
}
|
||
|
||
function activeWrapperForPath(structure, wrappers, objectName, path, beforeIndex) {
|
||
return wrappers
|
||
.filter(
|
||
(wrapper) =>
|
||
wrapper.kind === "path" &&
|
||
wrapper.name === objectName &&
|
||
pathsEqual(wrapper.path, path) &&
|
||
isStaticWrapperActiveAt(structure, wrapper, beforeIndex)
|
||
)
|
||
.toSorted((left, right) => right.end - left.end)[0];
|
||
}
|
||
|
||
function activeFunctionReturnWrapperForPath(structure, wrappers, functionName, path, beforeIndex) {
|
||
return wrappers
|
||
.filter(
|
||
(wrapper) =>
|
||
wrapper.kind === "functionReturnPath" &&
|
||
wrapper.name === functionName &&
|
||
pathsEqual(wrapper.path, path) &&
|
||
isStaticWrapperActiveAt(structure, wrapper, beforeIndex)
|
||
)
|
||
.toSorted((left, right) => right.end - left.end)[0];
|
||
}
|
||
|
||
function activeWrapperForDestructuringSource(content, structure, wrappers, sourceStart, path, beforeIndex) {
|
||
const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(sourceStart));
|
||
if (!nameMatch || isMemberIdentifierReference(structure, sourceStart)) return null;
|
||
|
||
const sourceName = nameMatch[0];
|
||
const afterName = skipWhitespaceForward(structure, sourceStart + sourceName.length);
|
||
const callEnd = staticZeroArgumentCallEndAt(content, structure, afterName);
|
||
if (callEnd !== -1) {
|
||
const wrapper = activeFunctionReturnWrapperForPath(structure, wrappers, sourceName, path, beforeIndex);
|
||
return wrapper ? { wrapper, end: callEnd } : null;
|
||
}
|
||
|
||
const wrapper = activeWrapperForPath(structure, wrappers, sourceName, path, beforeIndex);
|
||
return wrapper ? { wrapper, end: sourceStart + sourceName.length } : null;
|
||
}
|
||
|
||
function destructuringAliasBindingsForWrapperPaths(content, structure, bindingStart, bindingEnd, beforeIndex, basePath = []) {
|
||
const bindings = [];
|
||
|
||
for (const part of destructuringParts(structure, bindingStart, bindingEnd)) {
|
||
const propertyName = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex);
|
||
if (!propertyName) continue;
|
||
|
||
const keyStart = skipWhitespaceForward(structure, part.start);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, part.end);
|
||
if (keyEnd === -1) continue;
|
||
|
||
const path = basePath.concat(propertyName);
|
||
const afterKey = skipWhitespaceForward(structure, keyEnd);
|
||
|
||
if (structure[afterKey] === ":") {
|
||
const valueStart = skipWhitespaceForward(structure, afterKey + 1);
|
||
if (structure[valueStart] === "{") {
|
||
const nestedEnd = findMatchingBrace(structure, valueStart);
|
||
if (nestedEnd !== -1 && nestedEnd < part.end) {
|
||
bindings.push(...destructuringAliasBindingsForWrapperPaths(content, structure, valueStart + 1, nestedEnd, beforeIndex, path));
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const aliasName = destructuringAliasName(structure, part.start, part.end);
|
||
if (aliasName) bindings.push({ aliasName, path });
|
||
continue;
|
||
}
|
||
|
||
const aliasName = destructuringAliasName(structure, part.start, part.end);
|
||
if (aliasName) bindings.push({ aliasName, path });
|
||
}
|
||
|
||
return bindings;
|
||
}
|
||
|
||
function arrayDestructuringAliasBindingsForWrapperPaths(structure, bindingStart, bindingEnd, basePath = []) {
|
||
const bindings = [];
|
||
const parts = destructuringParts(structure, bindingStart, bindingEnd);
|
||
|
||
for (const [elementIndex, part] of parts.entries()) {
|
||
const valueStart = skipWhitespaceForward(structure, part.start);
|
||
const valueEnd = skipWhitespaceBackward(structure, part.end - 1) + 1;
|
||
if (valueStart >= valueEnd) continue;
|
||
|
||
const path = basePath.concat(elementIndex);
|
||
|
||
if (structure[valueStart] === "[") {
|
||
const nestedEnd = findMatchingBracket(structure, valueStart);
|
||
if (nestedEnd !== -1 && nestedEnd === valueEnd - 1) {
|
||
bindings.push(...arrayDestructuringAliasBindingsForWrapperPaths(structure, valueStart + 1, nestedEnd, path));
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(valueStart, valueEnd));
|
||
if (!aliasMatch) continue;
|
||
|
||
const aliasEnd = valueStart + aliasMatch[0].length;
|
||
if (skipWhitespaceForward(structure, aliasEnd) !== valueEnd) continue;
|
||
|
||
bindings.push({ aliasName: aliasMatch[0], path });
|
||
}
|
||
|
||
return bindings;
|
||
}
|
||
|
||
function findStaticWrapperDestructuredAliases(content, structure, wrappers) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s*\{/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", declarationMatch.index);
|
||
if (openBraceIndex === -1) continue;
|
||
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] === ":") {
|
||
const equalsAfterType = structure.indexOf("=", cursor + 1);
|
||
const semicolonAfterType = structure.indexOf(";", cursor + 1);
|
||
if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue;
|
||
cursor = skipWhitespaceForward(structure, equalsAfterType + 1);
|
||
} else {
|
||
if (structure[cursor] !== "=") continue;
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
const statementEnd = findStatementEnd(structure, cursor);
|
||
|
||
for (const binding of destructuringAliasBindingsForWrapperPaths(content, structure, openBraceIndex + 1, closeBraceIndex, declarationMatch.index)) {
|
||
const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, declarationMatch.index);
|
||
if (!source) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, source.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: binding.aliasName, index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findStaticWrapperArrayDestructuredAliases(content, structure, wrappers) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s*\[/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const openBracketIndex = structure.indexOf("[", declarationMatch.index);
|
||
if (openBracketIndex === -1) continue;
|
||
|
||
const closeBracketIndex = findMatchingBracket(structure, openBracketIndex);
|
||
if (closeBracketIndex === -1) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBracketIndex + 1);
|
||
if (structure[cursor] === ":") {
|
||
const equalsAfterType = structure.indexOf("=", cursor + 1);
|
||
const semicolonAfterType = structure.indexOf(";", cursor + 1);
|
||
if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue;
|
||
cursor = skipWhitespaceForward(structure, equalsAfterType + 1);
|
||
} else {
|
||
if (structure[cursor] !== "=") continue;
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
const statementEnd = findStatementEnd(structure, cursor);
|
||
|
||
for (const binding of arrayDestructuringAliasBindingsForWrapperPaths(structure, openBracketIndex + 1, closeBracketIndex)) {
|
||
const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, declarationMatch.index);
|
||
if (!source) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, source.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: binding.aliasName, index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findStaticWrapperAssignedDestructuredAliases(content, structure, wrappers) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\(\s*\{/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", assignmentMatch.index);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", openParenIndex);
|
||
if (openBraceIndex === -1) continue;
|
||
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] !== "=") continue;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
const statementEnd = findStatementEnd(structure, openParenIndex);
|
||
|
||
for (const binding of destructuringAliasBindingsForWrapperPaths(content, structure, openBraceIndex + 1, closeBraceIndex, assignmentMatch.index)) {
|
||
const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, assignmentMatch.index);
|
||
if (!source || skipWhitespaceForward(structure, source.end) !== closeParenIndex) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: binding.aliasName, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findStaticWrapperAssignedArrayDestructuredAliases(content, structure, wrappers) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\(\s*\[/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", assignmentMatch.index);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue;
|
||
|
||
const openBracketIndex = structure.indexOf("[", openParenIndex);
|
||
if (openBracketIndex === -1) continue;
|
||
|
||
const closeBracketIndex = findMatchingBracket(structure, openBracketIndex);
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeBracketIndex === -1 || closeParenIndex === -1 || closeBracketIndex > closeParenIndex) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBracketIndex + 1);
|
||
if (structure[cursor] !== "=") continue;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
const statementEnd = findStatementEnd(structure, openParenIndex);
|
||
|
||
for (const binding of arrayDestructuringAliasBindingsForWrapperPaths(structure, openBracketIndex + 1, closeBracketIndex)) {
|
||
const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, assignmentMatch.index);
|
||
if (!source || skipWhitespaceForward(structure, source.end) !== closeParenIndex) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: binding.aliasName, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findStaticWrapperExtractionAliases(content, structure, wrappers) {
|
||
return findStaticWrapperUsageAliases(content, structure, wrappers)
|
||
.concat(findStaticWrapperDestructuredAliases(content, structure, wrappers))
|
||
.concat(findStaticWrapperArrayDestructuredAliases(content, structure, wrappers))
|
||
.concat(findStaticWrapperAssignedDestructuredAliases(content, structure, wrappers))
|
||
.concat(findStaticWrapperAssignedArrayDestructuredAliases(content, structure, wrappers))
|
||
.toSorted((left, right) => left.end - right.end);
|
||
}
|
||
|
||
function staticBuiltinInvokerAliasAt(structure, aliases, aliasName, beforeIndex) {
|
||
return latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex);
|
||
}
|
||
|
||
function staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, beforeIndex) {
|
||
return staticBuiltinInvokerAliasAt(structure, aliases, aliasName, beforeIndex)?.kind ?? null;
|
||
}
|
||
|
||
function aliasDirectInvocationCallMatches(structure, aliasNames) {
|
||
const uniqueAliasNames = [...new Set(aliasNames)];
|
||
if (uniqueAliasNames.length === 0) return [];
|
||
|
||
const matches = [];
|
||
const aliasPattern = new RegExp(String.raw`\b(${uniqueAliasNames.map(escapeRegExp).join("|")})\b`, "g");
|
||
|
||
for (const match of structure.matchAll(aliasPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const aliasName = match[1];
|
||
const aliasIndex = match.index + match[0].indexOf(aliasName);
|
||
if (isMemberIdentifierReference(structure, aliasIndex)) continue;
|
||
|
||
let openParenIndex = skipWhitespaceForward(structure, aliasIndex + aliasName.length);
|
||
if (structure.startsWith("?.", openParenIndex)) {
|
||
openParenIndex = skipWhitespaceForward(structure, openParenIndex + 2);
|
||
}
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ aliasName, aliasIndex, openParenIndex, closeParenIndex });
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
function functionPrototypeMethodKindFromArgument(content, structure, argument, aliases) {
|
||
if (!argument) return null;
|
||
|
||
const directKind = functionPrototypeMethodKind(structure, argument.index, argument.end);
|
||
if (directKind) return directKind;
|
||
|
||
const aliasName = simpleIdentifierSourceName(structure, argument.index, argument.end);
|
||
if (!aliasName) return null;
|
||
|
||
const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, argument.index);
|
||
return aliasKind === "call" || aliasKind === "apply" ? aliasKind : null;
|
||
}
|
||
|
||
function functionPrototypeCallInvocationMatches(content, structure, aliases) {
|
||
const matches = [];
|
||
const directCallPattern = /\bFunction\b/g;
|
||
|
||
for (const match of structure.matchAll(directCallPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, match.index);
|
||
if (!baseMatch || (baseMatch.kind !== "call" && baseMatch.kind !== "apply")) continue;
|
||
|
||
const operation = staticBuiltinMemberOperationMatchAt(content, structure, baseMatch.end, ["call", "apply"]);
|
||
if (!operation) continue;
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, operation.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ index: match.index, methodKind: baseMatch.kind, mode: operation.value, openParenIndex, closeParenIndex });
|
||
}
|
||
|
||
const aliasNames = [...new Set(aliases.filter((alias) => (alias.kind === "call" || alias.kind === "apply") && !alias.bound).map((alias) => alias.name))];
|
||
if (aliasNames.length > 0) {
|
||
const aliasCallPattern = new RegExp(String.raw`\b(${aliasNames.map(escapeRegExp).join("|")})\b`, "g");
|
||
for (const match of structure.matchAll(aliasCallPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const aliasName = match[1];
|
||
const aliasIndex = match.index + match[0].indexOf(aliasName);
|
||
if (isMemberIdentifierReference(structure, aliasIndex)) continue;
|
||
|
||
const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex);
|
||
if (aliasKind !== "call" && aliasKind !== "apply") continue;
|
||
|
||
const operation = staticBuiltinMemberOperationMatchAt(content, structure, aliasIndex + aliasName.length, ["call", "apply"]);
|
||
if (!operation) continue;
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, operation.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ index: aliasIndex, methodKind: aliasKind, mode: operation.value, openParenIndex, closeParenIndex });
|
||
}
|
||
}
|
||
|
||
const boundAliasNames = [...new Set(aliases.filter((alias) => (alias.kind === "call" || alias.kind === "apply") && alias.bound).map((alias) => alias.name))];
|
||
if (boundAliasNames.length === 0) return matches.toSorted((left, right) => left.index - right.index);
|
||
|
||
for (const match of aliasDirectInvocationCallMatches(structure, boundAliasNames)) {
|
||
const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match;
|
||
const alias = staticBuiltinInvokerAliasAt(structure, aliases, aliasName, aliasIndex);
|
||
if (!alias?.bound || (alias.kind !== "call" && alias.kind !== "apply")) continue;
|
||
|
||
matches.push({ index: aliasIndex, methodKind: alias.kind, mode: "call", openParenIndex, closeParenIndex });
|
||
}
|
||
|
||
return matches.toSorted((left, right) => left.index - right.index);
|
||
}
|
||
|
||
function functionPrototypeEquivalentArgumentSource(content, structure, invocation, argumentIndex) {
|
||
if (invocation.mode === "call") {
|
||
return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex);
|
||
}
|
||
|
||
if (invocation.mode === "apply") {
|
||
if (argumentIndex === 0) {
|
||
return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 0);
|
||
}
|
||
|
||
const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1);
|
||
return nthArrayElementSource(content, structure, arrayArgument, argumentIndex - 1);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function functionPrototypeBoundTargetExpressionMatchAt(content, structure, index) {
|
||
const expressionStart = skipWhitespaceForward(structure, index);
|
||
const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, expressionStart);
|
||
if (!baseMatch || (baseMatch.kind !== "call" && baseMatch.kind !== "apply")) return null;
|
||
|
||
const bindMethodStart = skipWhitespaceForward(structure, baseMatch.end);
|
||
const bindMethodMatch = staticBuiltinMemberOperationMatchAt(content, structure, bindMethodStart, ["bind"]);
|
||
if (!bindMethodMatch) return null;
|
||
|
||
const bindCallStart = skipWhitespaceForward(structure, bindMethodMatch.end);
|
||
if (structure[bindCallStart] !== "(") return null;
|
||
|
||
const bindCallEnd = findMatchingParen(structure, bindCallStart);
|
||
if (bindCallEnd === -1) return null;
|
||
|
||
const targetArgument = nthCallArgumentSource(content, structure, bindCallStart, bindCallEnd, 0);
|
||
if (!targetArgument) return null;
|
||
|
||
return { methodKind: baseMatch.kind, targetArgument, end: bindCallEnd + 1 };
|
||
}
|
||
|
||
function findFunctionPrototypeBoundTargetAliases(content, structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, initializerStart);
|
||
if (!boundTargetMatch) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, boundTargetMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 绑定 forbidden target 的 wrapper alias 只跟踪简单同文件声明,复杂重赋值保持未知。
|
||
aliases.push({
|
||
name: declarationMatch[1],
|
||
methodKind: boundTargetMatch.methodKind,
|
||
targetArgument: boundTargetMatch.targetArgument,
|
||
index: declarationMatch.index,
|
||
end: aliasEnd
|
||
});
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, initializerStart);
|
||
if (!boundTargetMatch) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, boundTargetMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({
|
||
name: variableName,
|
||
methodKind: boundTargetMatch.methodKind,
|
||
targetArgument: boundTargetMatch.targetArgument,
|
||
index: assignmentMatch.index,
|
||
end: aliasEnd
|
||
});
|
||
}
|
||
|
||
return propagateStaticIdentifierAliases(structure, aliases, (sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
methodKind: sourceAlias.methodKind,
|
||
targetArgument: sourceAlias.targetArgument,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
}
|
||
|
||
function functionPrototypeBoundTargetAliasAt(structure, aliases, aliasName, beforeIndex) {
|
||
return latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex);
|
||
}
|
||
|
||
function functionPrototypeBoundTargetInvocationMatches(content, structure, aliases) {
|
||
const matches = [];
|
||
const directPattern = /\bFunction\b/g;
|
||
|
||
for (const match of structure.matchAll(directPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, match.index);
|
||
if (!boundTargetMatch) continue;
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, boundTargetMatch.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({
|
||
index: match.index,
|
||
methodKind: boundTargetMatch.methodKind,
|
||
targetArgument: boundTargetMatch.targetArgument,
|
||
openParenIndex,
|
||
closeParenIndex
|
||
});
|
||
}
|
||
|
||
const aliasNames = [...new Set(aliases.map((alias) => alias.name))];
|
||
if (aliasNames.length === 0) return matches;
|
||
|
||
for (const match of aliasDirectInvocationCallMatches(structure, aliasNames)) {
|
||
const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match;
|
||
const alias = functionPrototypeBoundTargetAliasAt(structure, aliases, aliasName, aliasIndex);
|
||
if (!alias) continue;
|
||
|
||
matches.push({
|
||
index: aliasIndex,
|
||
methodKind: alias.methodKind,
|
||
targetArgument: alias.targetArgument,
|
||
openParenIndex,
|
||
closeParenIndex
|
||
});
|
||
}
|
||
|
||
return matches.toSorted((left, right) => left.index - right.index);
|
||
}
|
||
|
||
function reflectApplyInvocationMatches(content, structure, aliases) {
|
||
const matches = [];
|
||
const directReflectApplyPattern = /\bReflect\b/g;
|
||
|
||
for (const match of structure.matchAll(directReflectApplyPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, match.index);
|
||
if (!baseMatch || baseMatch.kind !== "reflectApply") continue;
|
||
|
||
const operation = staticBuiltinMemberOperationMatchAt(content, structure, baseMatch.end, ["call", "apply"]);
|
||
if (operation) {
|
||
const openParenIndex = skipWhitespaceForward(structure, operation.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ index: match.index, mode: operation.value, openParenIndex, closeParenIndex });
|
||
continue;
|
||
}
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, baseMatch.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ index: match.index, mode: "direct", openParenIndex, closeParenIndex });
|
||
}
|
||
|
||
const aliasNames = [...new Set(aliases.filter((alias) => alias.kind === "reflectApply").map((alias) => alias.name))];
|
||
if (aliasNames.length === 0) return matches;
|
||
|
||
const secondOrderAliasNames = [...new Set(aliases.filter((alias) => alias.kind === "reflectApply" && !alias.bound).map((alias) => alias.name))];
|
||
const aliasSecondOrderPattern =
|
||
secondOrderAliasNames.length > 0
|
||
? new RegExp(String.raw`\b(${secondOrderAliasNames.map(escapeRegExp).join("|")})\b\s*(?:\.|\?\.)\s*(call|apply)\s*\(`, "g")
|
||
: null;
|
||
for (const match of aliasDirectInvocationCallMatches(structure, aliasNames)) {
|
||
const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match;
|
||
const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex);
|
||
if (aliasKind !== "reflectApply") continue;
|
||
|
||
matches.push({ index: aliasIndex, mode: "direct", openParenIndex, closeParenIndex });
|
||
}
|
||
|
||
if (aliasSecondOrderPattern) {
|
||
const aliasSecondOrderNamePattern = new RegExp(String.raw`\b(${secondOrderAliasNames.map(escapeRegExp).join("|")})\b`, "g");
|
||
for (const match of structure.matchAll(aliasSecondOrderNamePattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const aliasName = match[1];
|
||
const aliasIndex = match.index + match[0].indexOf(aliasName);
|
||
if (isMemberIdentifierReference(structure, aliasIndex)) continue;
|
||
|
||
const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex);
|
||
if (aliasKind !== "reflectApply") continue;
|
||
|
||
const operation = staticBuiltinMemberOperationMatchAt(content, structure, aliasIndex + aliasName.length, ["call", "apply"]);
|
||
if (!operation) continue;
|
||
|
||
const openParenIndex = skipWhitespaceForward(structure, operation.end);
|
||
if (structure[openParenIndex] !== "(") continue;
|
||
|
||
const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
matches.push({ index: aliasIndex, mode: operation.value, openParenIndex, closeParenIndex });
|
||
}
|
||
}
|
||
|
||
return matches.toSorted((left, right) => left.index - right.index);
|
||
}
|
||
|
||
function reflectApplyEquivalentArgumentSource(content, structure, invocation, argumentIndex) {
|
||
if (invocation.mode === "direct") {
|
||
return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex);
|
||
}
|
||
|
||
if (invocation.mode === "call") {
|
||
return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex + 1);
|
||
}
|
||
|
||
if (invocation.mode === "apply") {
|
||
const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1);
|
||
return nthArrayElementSource(content, structure, arrayArgument, argumentIndex);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function prismaModelMethodCallPattern(models, methods) {
|
||
return new RegExp(
|
||
`${staticPrismaPropertyAccessPattern(models)}\\s*${staticPrismaPropertyAccessPattern(methods)}\\s*(?:\\?\\.)?\\s*\\(`,
|
||
"g"
|
||
);
|
||
}
|
||
|
||
function findStaticPrismaDelegateAliases(content, structure, models) {
|
||
const aliases = [];
|
||
const declarationPattern = new RegExp(
|
||
String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=`,
|
||
"g"
|
||
);
|
||
|
||
for (const match of structure.matchAll(declarationPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", match.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchDelegateMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
(scanContent, scanStructure, sourceStart, sourceEnd) =>
|
||
completeStaticPrismaDelegateAliasExpressionMatch(scanContent, scanStructure, sourceStart, sourceEnd, models)
|
||
);
|
||
if (branchDelegateMatch?.branch) {
|
||
// 条件/逻辑分支里只要出现受保护 Prisma delegate,后续 alias.method(...) 必须按真实 delegate 写入扫描。
|
||
aliases.push({ name: match[1], index: match.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, initializerStart, models);
|
||
if (!delegateMatch) continue;
|
||
|
||
const nextIndex = skipWhitespaceForward(structure, delegateMatch.end);
|
||
// 只接受 `const jobs = db.job;` 这类完整 delegate alias,避免把 `db.job.update` 误判成 alias。
|
||
if (structure[nextIndex] === "." || structure[nextIndex] === "[" || structure[nextIndex] === "(" || structure[nextIndex] === "?") {
|
||
continue;
|
||
}
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
aliases.push({ name: match[1], index: match.index, end: aliasEnd });
|
||
}
|
||
|
||
aliases.push(...findStaticPrismaDestructuredDelegateAliases(content, structure, models));
|
||
aliases.push(...findStaticPrismaAssignedDestructuredDelegateAliases(content, structure, models));
|
||
aliases.push(...findStaticPrismaAssignedDelegateAliases(content, structure, models));
|
||
|
||
const directAliases = propagateStaticIdentifierAliases(structure, aliases, (_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
|
||
const wrappers = prismaDelegateWrapperSources(content, structure, models, directAliases);
|
||
directAliases.push(...findStaticWrapperExtractionAliases(content, structure, wrappers));
|
||
|
||
return propagateStaticIdentifierAliases(structure, directAliases, (_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
}
|
||
|
||
function findStaticPrismaDestructuredDelegateAliases(content, structure, models) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s*\{/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", declarationMatch.index);
|
||
if (openBraceIndex === -1) continue;
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, models, declarationMatch.index);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] === ":") {
|
||
const equalsAfterType = structure.indexOf("=", cursor + 1);
|
||
const semicolonAfterType = structure.indexOf(";", cursor + 1);
|
||
if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue;
|
||
cursor = skipWhitespaceForward(structure, equalsAfterType + 1);
|
||
} else {
|
||
if (structure[cursor] !== "=") continue;
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor);
|
||
if (!receiverMatch) continue;
|
||
|
||
// 解构 alias 只跟踪静态 Prisma client receiver,避免把运行时对象结构误判成 delegate。
|
||
const expressionEnd = cursor + receiverMatch[0].length;
|
||
const statementEnd = findStatementEnd(structure, cursor);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
for (const name of names) {
|
||
aliases.push({ name, index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findStaticPrismaAssignedDestructuredDelegateAliases(content, structure, models) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\(\s*\{/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", assignmentMatch.index);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", openParenIndex);
|
||
if (openBraceIndex === -1) continue;
|
||
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, models, assignmentMatch.index);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] !== "=") continue;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor);
|
||
if (!receiverMatch) continue;
|
||
|
||
const expressionEnd = cursor + receiverMatch[0].length;
|
||
if (skipWhitespaceForward(structure, expressionEnd) !== closeParenIndex) continue;
|
||
|
||
// 赋值式解构 alias 覆盖 `let versions; ({ gameVersion: versions } = db)`。
|
||
const statementEnd = findStatementEnd(structure, openParenIndex);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
for (const name of names) {
|
||
aliases.push({ name, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function isStaticAliasExpressionContinuation(structure, index) {
|
||
if (index >= structure.length) return false;
|
||
if (structure.startsWith("?.", index)) return true;
|
||
|
||
return "([.`+-/*%,?:&|<>=!".includes(structure[index]);
|
||
}
|
||
|
||
function simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd) {
|
||
const nextTokenIndex = skipWhitespaceForward(structure, expressionEnd);
|
||
if (nextTokenIndex === statementEnd) return statementEnd;
|
||
|
||
const gapAfterExpression = structure.slice(expressionEnd, nextTokenIndex);
|
||
if (!hasLineTerminator(gapAfterExpression)) return -1;
|
||
if (isStaticAliasExpressionContinuation(structure, nextTokenIndex)) return -1;
|
||
|
||
return expressionEnd;
|
||
}
|
||
|
||
function completeStaticPrismaDelegateAliasExpressionMatch(content, structure, sourceStart, sourceEnd, models) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, expressionStart, models);
|
||
if (!delegateMatch || delegateMatch.end > expressionEnd) return null;
|
||
if (!isAtStaticWrapperExpressionEnd(structure, delegateMatch.end, expressionEnd)) return null;
|
||
|
||
return { index: expressionStart, end: expressionEnd };
|
||
}
|
||
|
||
function findStaticPrismaAssignedDelegateAliases(content, structure, models) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchDelegateMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
(scanContent, scanStructure, sourceStart, sourceEnd) =>
|
||
completeStaticPrismaDelegateAliasExpressionMatch(scanContent, scanStructure, sourceStart, sourceEnd, models)
|
||
);
|
||
if (branchDelegateMatch?.branch) {
|
||
// 后续赋值同样按 fail-closed 处理 mixed ternary/logical delegate source。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, initializerStart, models);
|
||
if (!delegateMatch) continue;
|
||
if (delegateMatch.end > statementEnd) continue;
|
||
|
||
const nextIndex = skipWhitespaceForward(structure, delegateMatch.end);
|
||
if (structure[nextIndex] === "." || structure[nextIndex] === "[" || structure[nextIndex] === "(" || structure[nextIndex] === "?") {
|
||
continue;
|
||
}
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 后置赋值 alias 只接受 `jobs = db.job` 这种完整 delegate,避免比较、箭头或复杂表达式误判。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function prismaDelegateSourceMatcher(models, aliasResolver = null) {
|
||
return (content, structure, sourceStart, sourceEnd) => {
|
||
const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, sourceStart, models);
|
||
if (delegateMatch && isAtStaticWrapperExpressionEnd(structure, delegateMatch.end, sourceEnd)) return delegateMatch;
|
||
|
||
return aliasResolver ? aliasResolver(structure, sourceStart, sourceEnd) : null;
|
||
};
|
||
}
|
||
|
||
function prismaDelegateWrapperSources(content, structure, models, aliases = []) {
|
||
const aliasResolver = (scanStructure, sourceStart, sourceEnd) =>
|
||
staticIdentifierAliasSourceMatchAt(scanStructure, aliases, sourceStart, sourceEnd, sourceStart);
|
||
return staticWrapperSources(content, structure, prismaDelegateSourceMatcher(models, aliasResolver));
|
||
}
|
||
|
||
function prismaModelMethodSourceMatcher(models, methods, aliases, methodAliases = []) {
|
||
return (content, structure, sourceStart, sourceEnd) => {
|
||
const methodAccess = staticPrismaModelMethodAccessMatchAt(content, structure, sourceStart, models, methods, aliases);
|
||
if (methodAccess) {
|
||
if (isAtStaticWrapperExpressionEnd(structure, methodAccess.end, sourceEnd)) return methodAccess;
|
||
|
||
const bindCloseParenIndex = prismaMethodBindCloseParenIndex(content, structure, methodAccess.end, sourceEnd);
|
||
// 函数返回或 Object.assign wrapper 里出现 `db.model.method.bind(db.model)` 时,后续调用仍等价于直接 Prisma 写。
|
||
if (bindCloseParenIndex !== -1 && isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, sourceEnd)) {
|
||
return { index: methodAccess.index, end: bindCloseParenIndex + 1 };
|
||
}
|
||
}
|
||
|
||
return staticIdentifierAliasSourceMatchAt(structure, methodAliases, sourceStart, sourceEnd, sourceStart);
|
||
};
|
||
}
|
||
|
||
function prismaModelMethodWrapperSources(content, structure, models, methods, aliases, methodAliases = []) {
|
||
return staticWrapperSources(content, structure, prismaModelMethodSourceMatcher(models, methods, aliases, methodAliases));
|
||
}
|
||
|
||
function prismaModelMethodWrapperAliases(content, structure, methodWrappers) {
|
||
return propagateStaticIdentifierAliases(
|
||
structure,
|
||
findStaticWrapperExtractionAliases(content, structure, methodWrappers),
|
||
(_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
})
|
||
);
|
||
}
|
||
|
||
function staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, index, models, aliases = []) {
|
||
const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, index, models);
|
||
if (delegateMatch) return delegateMatch;
|
||
|
||
const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(index));
|
||
if (!aliasMatch || isMemberIdentifierReference(structure, index)) return null;
|
||
|
||
const aliasName = aliasMatch[0];
|
||
if (!latestStaticAliasRecordAt(structure, aliases, aliasName, index)) return null;
|
||
|
||
return { index, end: index + aliasName.length };
|
||
}
|
||
|
||
function findStaticPrismaDestructuredModelMethodAliases(content, structure, models, methods, aliases) {
|
||
const methodAliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s*\{/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", declarationMatch.index);
|
||
if (openBraceIndex === -1) continue;
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, methods, declarationMatch.index);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] === ":") {
|
||
const equalsAfterType = structure.indexOf("=", cursor + 1);
|
||
const semicolonAfterType = structure.indexOf(";", cursor + 1);
|
||
if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue;
|
||
cursor = skipWhitespaceForward(structure, equalsAfterType + 1);
|
||
} else {
|
||
if (structure[cursor] !== "=") continue;
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
const delegateMatch = staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, cursor, models, aliases);
|
||
if (!delegateMatch) continue;
|
||
|
||
const statementEnd = findStatementEnd(structure, cursor);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// Prisma delegate method destructuring,例如 `const { delete: del } = db.auditLog`。
|
||
for (const name of names) {
|
||
methodAliases.push({ name, index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return methodAliases;
|
||
}
|
||
|
||
function findStaticPrismaAssignedDestructuredModelMethodAliases(content, structure, models, methods, aliases) {
|
||
const methodAliases = [];
|
||
const assignmentPattern = /\(\s*\{/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", assignmentMatch.index);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", openParenIndex);
|
||
if (openBraceIndex === -1) continue;
|
||
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, methods, assignmentMatch.index);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] !== "=") continue;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
const delegateMatch = staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, cursor, models, aliases);
|
||
if (!delegateMatch || skipWhitespaceForward(structure, delegateMatch.end) !== closeParenIndex) continue;
|
||
|
||
const statementEnd = findStatementEnd(structure, openParenIndex);
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 赋值式 method destructuring,例如 `let del; ({ delete: del } = db.auditLog)`。
|
||
for (const name of names) {
|
||
methodAliases.push({ name, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return methodAliases;
|
||
}
|
||
|
||
function findStaticPrismaModelMethodBoundAliases(content, structure, models, methods, aliases, methodWrappers = [], methodAliases = []) {
|
||
const boundAliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const bindAliasEnd = staticPrismaModelMethodBoundAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (bindAliasEnd === -1) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, bindAliasEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// Prisma method `.bind(delegate)` alias 后续等价于直接 method invocation。
|
||
boundAliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const bindAliasEnd = staticPrismaModelMethodBoundAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (bindAliasEnd === -1) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, bindAliasEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 后续赋值 alias 覆盖 `let update; update = db.model.method.bind(db.model)`,保持和声明式 bound alias 同一语义。
|
||
boundAliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
return boundAliases;
|
||
}
|
||
|
||
function staticPrismaModelMethodBoundAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
) {
|
||
const completeMatch = completeStaticPrismaModelMethodBoundAliasExpressionMatch(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (completeMatch) return completeMatch.end;
|
||
|
||
const branchMatches = staticExpressionBranchSources(structure, sourceStart, sourceEnd)
|
||
.map((branch) =>
|
||
completeStaticPrismaModelMethodBoundAliasExpressionMatch(
|
||
content,
|
||
structure,
|
||
branch.index,
|
||
branch.end,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
)
|
||
)
|
||
.filter(Boolean);
|
||
|
||
// 条件/逻辑表达式中任一静态分支是 guarded/audit/job method bind,后续 callable alias 按违规路径扫描。
|
||
return branchMatches.length > 0 ? sourceEnd : -1;
|
||
}
|
||
|
||
function completeStaticPrismaModelMethodBoundAliasExpressionMatch(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
const methodAccess = staticPrismaModelMethodAccessMatchAt(
|
||
content,
|
||
structure,
|
||
expressionStart,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (!methodAccess || methodAccess.end > expressionEnd) return null;
|
||
|
||
const bindCloseParenIndex = prismaMethodBindCloseParenIndex(content, structure, methodAccess.end, expressionEnd);
|
||
if (bindCloseParenIndex === -1) return null;
|
||
// bind helper 返回的是右括号下标;这里用 exclusive end 严格校验,避免越过 ternary 分支边界的空白继续扫到 `:`。
|
||
const bindExpressionEnd = bindCloseParenIndex + 1;
|
||
if (bindExpressionEnd !== expressionEnd) return null;
|
||
|
||
return { index: expressionStart, end: expressionEnd };
|
||
}
|
||
|
||
function completeStaticPrismaModelMethodPlainAliasExpressionMatch(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
const methodAccess = staticPrismaModelMethodAccessMatchAt(
|
||
content,
|
||
structure,
|
||
expressionStart,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (!methodAccess || methodAccess.end > expressionEnd) return null;
|
||
if (!isAtStaticWrapperExpressionEnd(structure, methodAccess.end, expressionEnd)) return null;
|
||
|
||
return { index: expressionStart, end: expressionEnd };
|
||
}
|
||
|
||
function staticPrismaModelMethodPlainAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
) {
|
||
const plainMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
sourceStart,
|
||
sourceEnd,
|
||
(scanContent, scanStructure, branchStart, branchEnd) =>
|
||
completeStaticPrismaModelMethodPlainAliasExpressionMatch(
|
||
scanContent,
|
||
scanStructure,
|
||
branchStart,
|
||
branchEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
)
|
||
);
|
||
|
||
// 条件/逻辑表达式中任一静态分支是 Prisma plain method,后续 callable alias 仍按 Prisma 写入扫描。
|
||
return plainMatch ? (plainMatch.branch ? sourceEnd : plainMatch.end) : -1;
|
||
}
|
||
|
||
function findStaticPrismaModelMethodPlainAliases(content, structure, models, methods, aliases, methodWrappers = [], methodAliases = []) {
|
||
const plainAliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const plainAliasEnd = staticPrismaModelMethodPlainAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (plainAliasEnd === -1) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, plainAliasEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// `const del = db.job.delete` 这类 plain method alias 后续通过 direct/call/apply/bind 都等价于 Prisma method invocation。
|
||
plainAliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const variableName = assignmentMatch[1];
|
||
const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName);
|
||
if (isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const plainAliasEnd = staticPrismaModelMethodPlainAliasInitializerEnd(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (plainAliasEnd === -1) continue;
|
||
|
||
const aliasEnd = simpleStaticAliasInitializerEnd(structure, plainAliasEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 后续赋值 alias 覆盖 `let update; update = db.gameVersion.update`,保持和声明式 plain alias 同一语义。
|
||
plainAliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
|
||
return plainAliases;
|
||
}
|
||
|
||
function staticExpressionBranchSources(structure, sourceStart, sourceEnd) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return [];
|
||
|
||
const ternary = topLevelTernaryExpressionParts(structure, expressionStart, expressionEnd);
|
||
if (ternary) return [ternary.whenTrue, ternary.whenFalse];
|
||
|
||
const logical = topLevelLogicalExpressionParts(structure, expressionStart, expressionEnd);
|
||
return logical ?? [];
|
||
}
|
||
|
||
function staticInitializerBranchMatch(content, structure, sourceStart, sourceEnd, matcher) {
|
||
const completeMatch = matcher(content, structure, sourceStart, sourceEnd);
|
||
if (completeMatch) return completeMatch;
|
||
|
||
for (const branch of staticExpressionBranchSources(structure, sourceStart, sourceEnd)) {
|
||
// 分支表达式递归展开,确保 `a ? safe : (b || db.model.method)` 这类嵌套静态分支不会 fail-open。
|
||
const branchMatch = staticInitializerBranchMatch(content, structure, branch.index, branch.end, matcher);
|
||
if (branchMatch) return { ...branchMatch, branch };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function topLevelTernaryExpressionParts(structure, sourceStart, sourceEnd) {
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
let questionIndex = -1;
|
||
|
||
for (let cursor = sourceStart; cursor < sourceEnd; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue;
|
||
|
||
if (char === "?" && structure[cursor + 1] !== ".") {
|
||
questionIndex = cursor;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (questionIndex === -1) return null;
|
||
|
||
parenDepth = 0;
|
||
braceDepth = 0;
|
||
bracketDepth = 0;
|
||
let nestedQuestionDepth = 0;
|
||
|
||
for (let cursor = questionIndex + 1; cursor < sourceEnd; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue;
|
||
|
||
if (char === "?" && structure[cursor + 1] !== ".") {
|
||
nestedQuestionDepth += 1;
|
||
continue;
|
||
}
|
||
|
||
if (char !== ":") continue;
|
||
if (nestedQuestionDepth > 0) {
|
||
nestedQuestionDepth -= 1;
|
||
continue;
|
||
}
|
||
|
||
return {
|
||
whenTrue: { index: skipWhitespaceForward(structure, questionIndex + 1), end: skipWhitespaceBackward(structure, cursor - 1) + 1 },
|
||
whenFalse: { index: skipWhitespaceForward(structure, cursor + 1), end: sourceEnd }
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function topLevelLogicalExpressionParts(structure, sourceStart, sourceEnd) {
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
|
||
for (let cursor = sourceStart; cursor < sourceEnd - 1; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue;
|
||
|
||
const operator = structure.slice(cursor, cursor + 2);
|
||
if (operator !== "||" && operator !== "&&") continue;
|
||
|
||
return [
|
||
{ index: sourceStart, end: skipWhitespaceBackward(structure, cursor - 1) + 1 },
|
||
{ index: skipWhitespaceForward(structure, cursor + 2), end: sourceEnd }
|
||
];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function prismaMethodBindCloseParenIndex(content, structure, methodAccessEnd, statementEnd) {
|
||
const operation = staticCallableMemberOperationMatchAt(content, structure, methodAccessEnd, ["bind"]);
|
||
if (!operation) return -1;
|
||
if (operation.closeParenIndex === -1 || operation.closeParenIndex > statementEnd) return -1;
|
||
return operation.closeParenIndex;
|
||
}
|
||
|
||
function prismaDelegateAliasMethodCallPattern(aliasNames, methods) {
|
||
const aliases = aliasNames.map(escapeRegExp).join("|");
|
||
return new RegExp(String.raw`\b(${aliases})\b\s*${staticPrismaPropertyAccessPattern(methods)}\s*(?:\?\.)?\s*\(`, "g");
|
||
}
|
||
|
||
function staticPrismaModelMethodAccessMatchAt(content, structure, index, models, methods, aliases = [], methodWrappers = [], methodAliases = []) {
|
||
const accessStart = skipWhitespaceForward(structure, index);
|
||
|
||
const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(accessStart));
|
||
if (receiverMatch) {
|
||
const modelAccessStart = skipWhitespaceForward(structure, accessStart + receiverMatch[0].length);
|
||
const modelAccess = staticPrismaPropertyAccessMatchAt(content, structure, modelAccessStart, models, modelAccessStart);
|
||
if (modelAccess) {
|
||
const methodAccessStart = skipWhitespaceForward(structure, modelAccess.end);
|
||
const methodAccess = staticPrismaPropertyAccessMatchAt(content, structure, methodAccessStart, methods, methodAccessStart);
|
||
if (methodAccess) return { index: modelAccess.index, end: methodAccess.end };
|
||
}
|
||
}
|
||
|
||
const wrapperUsage = staticWrapperSourceUsageMatchAt(content, structure, methodWrappers, accessStart, accessStart);
|
||
if (wrapperUsage) return { index: wrapperUsage.index, end: wrapperUsage.end };
|
||
|
||
const methodAliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(accessStart));
|
||
if (methodAliasMatch && !isMemberIdentifierReference(structure, accessStart)) {
|
||
const aliasName = methodAliasMatch[0];
|
||
if (latestStaticAliasRecordAt(structure, methodAliases, aliasName, accessStart)) {
|
||
return { index: accessStart, end: accessStart + aliasName.length };
|
||
}
|
||
}
|
||
|
||
const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(accessStart));
|
||
if (!aliasMatch || aliases.length === 0) return null;
|
||
|
||
const aliasName = aliasMatch[0];
|
||
if (!latestStaticAliasRecordAt(structure, aliases, aliasName, accessStart)) return null;
|
||
if (isMemberIdentifierReference(structure, accessStart)) return null;
|
||
|
||
const methodAccessStart = skipWhitespaceForward(structure, accessStart + aliasName.length);
|
||
const methodAccess = staticPrismaPropertyAccessMatchAt(content, structure, methodAccessStart, methods, methodAccessStart);
|
||
return methodAccess ? { index: accessStart, end: methodAccess.end } : null;
|
||
}
|
||
|
||
function staticPrismaModelMethodAccessArgumentMatch(content, structure, argument, models, methods, aliases, methodWrappers = [], methodAliases = []) {
|
||
if (!argument) return null;
|
||
|
||
const targetStart = skipWhitespaceForward(structure, argument.index);
|
||
const targetEnd = skipWhitespaceBackward(structure, argument.end - 1) + 1;
|
||
if (targetStart >= targetEnd) return null;
|
||
|
||
const accessMatch = staticPrismaModelMethodAccessMatchAt(
|
||
content,
|
||
structure,
|
||
targetStart,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (!accessMatch) return null;
|
||
|
||
return skipWhitespaceForward(structure, accessMatch.end) === targetEnd ? accessMatch : null;
|
||
}
|
||
|
||
function prismaMethodEquivalentInvocationMatch(content, structure, methodAccessEnd) {
|
||
const callStart = hasOptionalCallOperatorAt(structure, methodAccessEnd);
|
||
if (structure[callStart] === "(") return { callStart };
|
||
|
||
const operation = staticCallableMemberOperationMatchAt(content, structure, methodAccessEnd, ["call", "apply", "bind"]);
|
||
if (!operation) return null;
|
||
|
||
if (operation.kind === "call" || operation.kind === "apply") {
|
||
return { callStart: operation.openParenIndex };
|
||
}
|
||
|
||
let boundCallStart = skipWhitespaceForward(structure, operation.closeParenIndex + 1);
|
||
if (structure.startsWith("?.", boundCallStart)) {
|
||
boundCallStart = skipWhitespaceForward(structure, boundCallStart + 2);
|
||
}
|
||
if (structure[boundCallStart] !== "(") return null;
|
||
|
||
const boundCallEnd = findMatchingParen(structure, boundCallStart);
|
||
return boundCallEnd === -1 ? null : { callStart: boundCallStart };
|
||
}
|
||
|
||
function prismaFunctionPrototypeMethodCallMatchesInStructure(content, structure, models, methods, aliases, builtinInvokerAliases, methodWrappers, methodAliases) {
|
||
const matches = [];
|
||
const boundTargetAliases = findFunctionPrototypeBoundTargetAliases(content, structure);
|
||
|
||
for (const invocation of functionPrototypeCallInvocationMatches(content, structure, builtinInvokerAliases)) {
|
||
const targetArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 0);
|
||
const targetAccess = staticPrismaModelMethodAccessArgumentMatch(content, structure, targetArgument, models, methods, aliases, methodWrappers, methodAliases);
|
||
if (!targetAccess) continue;
|
||
|
||
// Function.prototype.call/apply 的目标函数在参数位;命中后复用外层调用体做 Prisma 写入扫描。
|
||
matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex });
|
||
}
|
||
|
||
for (const invocation of reflectApplyInvocationMatches(content, structure, builtinInvokerAliases)) {
|
||
const reflectTargetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 0);
|
||
if (!functionPrototypeMethodKindFromArgument(content, structure, reflectTargetArgument, builtinInvokerAliases)) {
|
||
const reflectTargetAccess = staticPrismaModelMethodAccessArgumentMatch(
|
||
content,
|
||
structure,
|
||
reflectTargetArgument,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (reflectTargetAccess) {
|
||
// Reflect.apply(method, receiver, [args]) 等价执行 Prisma method target。
|
||
matches.push({ index: reflectTargetAccess.index, callStart: invocation.openParenIndex });
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const targetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 1);
|
||
const targetAccess = staticPrismaModelMethodAccessArgumentMatch(content, structure, targetArgument, models, methods, aliases, methodWrappers, methodAliases);
|
||
if (!targetAccess) continue;
|
||
|
||
matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex });
|
||
}
|
||
|
||
for (const invocation of functionPrototypeBoundTargetInvocationMatches(content, structure, boundTargetAliases)) {
|
||
const targetAccess = staticPrismaModelMethodAccessArgumentMatch(
|
||
content,
|
||
structure,
|
||
invocation.targetArgument,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
);
|
||
if (!targetAccess) continue;
|
||
|
||
// Function.prototype.call/apply.bind(method)(...) 绑定的是真实 Prisma method target。
|
||
matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex });
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
function prismaModelMethodCallMatchesInStructure(content, structure, models, methods) {
|
||
const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure);
|
||
const matches = [];
|
||
|
||
for (const modelAccess of staticPrismaPropertyAccessMatches(content, scanStructure, models)) {
|
||
const methodAccessStart = skipWhitespaceForward(scanStructure, modelAccess.end);
|
||
const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart);
|
||
if (!methodAccess) continue;
|
||
|
||
const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end);
|
||
if (invocation) matches.push({ index: modelAccess.index, callStart: invocation.callStart });
|
||
}
|
||
|
||
const aliases = findStaticPrismaDelegateAliases(content, scanStructure, models);
|
||
const builtinInvokerAliases = findStaticBuiltinInvokerAliases(content, scanStructure);
|
||
const delegateWrappers = prismaDelegateWrapperSources(content, scanStructure, models, aliases);
|
||
const initialMethodWrappers = prismaModelMethodWrapperSources(content, scanStructure, models, methods, aliases);
|
||
const initialMethodAliases = prismaModelMethodWrapperAliases(content, scanStructure, initialMethodWrappers)
|
||
.concat(findStaticPrismaDestructuredModelMethodAliases(content, scanStructure, models, methods, aliases))
|
||
.concat(findStaticPrismaAssignedDestructuredModelMethodAliases(content, scanStructure, models, methods, aliases))
|
||
.concat(findStaticPrismaModelMethodPlainAliases(content, scanStructure, models, methods, aliases, initialMethodWrappers));
|
||
const boundMethodAliases = findStaticPrismaModelMethodBoundAliases(
|
||
content,
|
||
scanStructure,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
initialMethodWrappers,
|
||
initialMethodAliases
|
||
);
|
||
const methodAliases = propagateStaticIdentifierAliases(
|
||
scanStructure,
|
||
initialMethodAliases.concat(boundMethodAliases),
|
||
(_sourceAlias, initializer) => ({ name: initializer.name, index: initializer.index, end: initializer.end })
|
||
);
|
||
const methodWrappers = prismaModelMethodWrapperSources(content, scanStructure, models, methods, aliases, methodAliases);
|
||
methodAliases.push(...prismaModelMethodWrapperAliases(content, scanStructure, methodWrappers));
|
||
|
||
matches.push(
|
||
...prismaFunctionPrototypeMethodCallMatchesInStructure(
|
||
content,
|
||
scanStructure,
|
||
models,
|
||
methods,
|
||
aliases,
|
||
builtinInvokerAliases,
|
||
methodWrappers,
|
||
methodAliases
|
||
)
|
||
);
|
||
|
||
for (const delegateWrapperUsage of staticWrapperSourceUsageMatches(content, scanStructure, delegateWrappers)) {
|
||
const methodAccessStart = skipWhitespaceForward(scanStructure, delegateWrapperUsage.end);
|
||
const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart);
|
||
if (!methodAccess) continue;
|
||
|
||
const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end);
|
||
if (invocation) matches.push({ index: delegateWrapperUsage.index, callStart: invocation.callStart });
|
||
}
|
||
|
||
for (const methodWrapperUsage of staticWrapperSourceUsageMatches(content, scanStructure, methodWrappers)) {
|
||
const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodWrapperUsage.end);
|
||
if (invocation) matches.push({ index: methodWrapperUsage.index, callStart: invocation.callStart });
|
||
}
|
||
|
||
const methodAliasNames = [...new Set(methodAliases.map((alias) => alias.name))];
|
||
const methodAliasPattern =
|
||
methodAliasNames.length > 0 ? new RegExp(String.raw`\b(${methodAliasNames.map(escapeRegExp).join("|")})\b`, "g") : null;
|
||
|
||
if (methodAliasPattern) {
|
||
for (const match of scanStructure.matchAll(methodAliasPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const aliasName = match[1];
|
||
if (!latestStaticAliasRecordAt(scanStructure, methodAliases, aliasName, match.index)) continue;
|
||
if (isMemberIdentifierReference(scanStructure, match.index)) continue;
|
||
|
||
const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, match.index + aliasName.length);
|
||
if (invocation) matches.push({ index: match.index, callStart: invocation.callStart });
|
||
}
|
||
}
|
||
|
||
const aliasNames = [...new Set(aliases.map((alias) => alias.name))];
|
||
const aliasPattern = aliasNames.length > 0 ? new RegExp(String.raw`\b(${aliasNames.map(escapeRegExp).join("|")})\b`, "g") : null;
|
||
|
||
if (aliasPattern) {
|
||
for (const match of scanStructure.matchAll(aliasPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const aliasName = match[1];
|
||
if (!latestStaticAliasRecordAt(scanStructure, aliases, aliasName, match.index)) continue;
|
||
if (isMemberIdentifierReference(scanStructure, match.index)) continue;
|
||
|
||
const methodAccessStart = skipWhitespaceForward(scanStructure, match.index + aliasName.length);
|
||
const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart);
|
||
if (!methodAccess) continue;
|
||
|
||
const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end);
|
||
if (invocation) matches.push({ index: match.index, callStart: invocation.callStart });
|
||
}
|
||
}
|
||
|
||
return matches.toSorted((left, right) => left.index - right.index);
|
||
}
|
||
|
||
function prismaModelMethodCallMatches(content, models, methods) {
|
||
const structure = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
return prismaModelMethodCallMatchesInStructure(content, structure, models, methods);
|
||
}
|
||
|
||
function sqlTableListContainingPattern(targetTableReference) {
|
||
const tableReference = sqlAnyTableReferencePattern();
|
||
const tableListItem = String.raw`(?:ONLY\s+)?${tableReference}(?:\s*\*)?`;
|
||
const targetTableListItem = String.raw`(?:ONLY\s+)?${targetTableReference}\b(?:\s*\*)?`;
|
||
// 表列表允许目标表出现在任意位置,避免只匹配列表首尾造成绕过。
|
||
return String.raw`(?:${tableListItem}\s*,\s*)*${targetTableListItem}(?:\s*,\s*${tableListItem})*`;
|
||
}
|
||
|
||
function sqlTruncateTableListPattern(targetTableReference) {
|
||
return String.raw`\bTRUNCATE(?:\s+TABLE)?\s+${sqlTableListContainingPattern(targetTableReference)}`;
|
||
}
|
||
|
||
function sqlDropTableListPattern(targetTableReference) {
|
||
return String.raw`\bDROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?${sqlTableListContainingPattern(targetTableReference)}`;
|
||
}
|
||
|
||
function stripStringLiterals(content) {
|
||
return content
|
||
.replace(/"(?:\\.|[^"\\])*"/gs, '""')
|
||
.replace(/'(?:\\.|[^'\\])*'/gs, "''")
|
||
.replace(/`(?:\\.|[^`\\])*`/gs, "``");
|
||
}
|
||
|
||
function skipWhitespaceForward(content, index) {
|
||
let cursor = index;
|
||
while (cursor < content.length && /\s/.test(content[cursor])) cursor += 1;
|
||
return cursor;
|
||
}
|
||
|
||
function skipWhitespaceBackward(content, index) {
|
||
let cursor = index;
|
||
while (cursor >= 0 && /\s/.test(content[cursor])) cursor -= 1;
|
||
return cursor;
|
||
}
|
||
|
||
function isStaticQuotedPropertyKeyOrAccess(content, literalStart, literalEnd) {
|
||
const staticComputedSpan = staticComputedStringPropertyAccessSpan(content, literalStart);
|
||
if (staticComputedSpan) return true;
|
||
|
||
const afterLiteral = skipWhitespaceForward(content, literalEnd);
|
||
if (content[afterLiteral] === ":") return true;
|
||
|
||
const afterComputedKey = skipWhitespaceForward(content, afterLiteral + 1);
|
||
if (content[afterLiteral] !== "]") return false;
|
||
|
||
const beforeLiteral = skipWhitespaceBackward(content, literalStart - 1);
|
||
if (content[beforeLiteral] !== "[") return false;
|
||
|
||
// 保留静态 bracket key,覆盖对象 key 与 Prisma property access 两种源码形态。
|
||
return (
|
||
content[afterComputedKey] === ":" ||
|
||
content[afterComputedKey] === "." ||
|
||
content[afterComputedKey] === "?" ||
|
||
content[afterComputedKey] === "[" ||
|
||
content[afterComputedKey] === "(" ||
|
||
content[afterComputedKey] === ";" ||
|
||
content[afterComputedKey] === "," ||
|
||
content[afterComputedKey] === ")" ||
|
||
content[afterComputedKey] === "}" ||
|
||
typeof content[afterComputedKey] === "undefined"
|
||
);
|
||
}
|
||
|
||
function staticComputedStringPropertyAccessSpan(content, literalStart) {
|
||
const openBracketIndex = content.lastIndexOf("[", literalStart);
|
||
if (openBracketIndex === -1) return null;
|
||
if (content.lastIndexOf("]", literalStart) > openBracketIndex) return null;
|
||
|
||
const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(content));
|
||
const closeBracketIndex = findMatchingBracket(structure, openBracketIndex);
|
||
if (closeBracketIndex === -1 || literalStart > closeBracketIndex) return null;
|
||
|
||
const afterComputedKey = skipWhitespaceForward(content, closeBracketIndex + 1);
|
||
if (
|
||
!(
|
||
content[afterComputedKey] === ":" ||
|
||
content[afterComputedKey] === "." ||
|
||
content[afterComputedKey] === "?" ||
|
||
content[afterComputedKey] === "[" ||
|
||
content[afterComputedKey] === "(" ||
|
||
content[afterComputedKey] === ";" ||
|
||
content[afterComputedKey] === "," ||
|
||
content[afterComputedKey] === ")" ||
|
||
content[afterComputedKey] === "}" ||
|
||
typeof content[afterComputedKey] === "undefined"
|
||
)
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
const expressionStart = openBracketIndex + 1;
|
||
const expressionEnd = closeBracketIndex;
|
||
const values = staticJavaScriptExpressionValues(
|
||
content,
|
||
structure,
|
||
content.slice(expressionStart, expressionEnd),
|
||
expressionStart,
|
||
openBracketIndex,
|
||
new Set(),
|
||
0
|
||
);
|
||
|
||
return values.length === 1 ? { start: openBracketIndex, end: closeBracketIndex + 1 } : null;
|
||
}
|
||
|
||
function stripStringLiteralsExceptPropertyKeys(content) {
|
||
let output = "";
|
||
|
||
for (let index = 0; index < content.length; index += 1) {
|
||
const quote = content[index];
|
||
if (quote === "/" && isRegexLiteralStart(content, index)) {
|
||
const literalEnd = findRegexLiteralEnd(content, index);
|
||
if (literalEnd !== -1) {
|
||
output += "/".padEnd(literalEnd - index, " ") + "/";
|
||
index = literalEnd;
|
||
continue;
|
||
}
|
||
}
|
||
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
||
output += quote;
|
||
continue;
|
||
}
|
||
|
||
const literalStart = index;
|
||
let literalEnd = literalStart + 1;
|
||
while (literalEnd < content.length) {
|
||
const char = content[literalEnd];
|
||
if (char === "\\") {
|
||
literalEnd += 2;
|
||
continue;
|
||
}
|
||
literalEnd += 1;
|
||
if (char === quote) break;
|
||
}
|
||
|
||
const literal = content.slice(literalStart, literalEnd);
|
||
// 只保留静态 key/access,普通字符串值仍等长剥离,避免破坏源码索引。
|
||
output += isStaticQuotedPropertyKeyOrAccess(content, literalStart, literalEnd)
|
||
? literal
|
||
: `${quote}${" ".repeat(Math.max(0, literal.length - 2))}${quote}`;
|
||
index = literalEnd - 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function maskCommentsOutsideStringLiterals(content) {
|
||
let output = "";
|
||
let index = 0;
|
||
|
||
while (index < content.length) {
|
||
const char = content[index];
|
||
const next = content[index + 1];
|
||
|
||
if (char === "/" && next === "/") {
|
||
output += " ";
|
||
index += 2;
|
||
while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
|
||
output += " ";
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (char === "/" && next === "*") {
|
||
output += " ";
|
||
index += 2;
|
||
while (index < content.length) {
|
||
const blockChar = content[index];
|
||
const blockNext = content[index + 1];
|
||
if (blockChar === "*" && blockNext === "/") {
|
||
output += " ";
|
||
index += 2;
|
||
break;
|
||
}
|
||
output += blockChar === "\n" || blockChar === "\r" ? blockChar : " ";
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (char === "/" && isRegexLiteralStart(content, index)) {
|
||
const literalEnd = findRegexLiteralEnd(content, index);
|
||
if (literalEnd !== -1) {
|
||
output += content.slice(index, literalEnd + 1);
|
||
index = literalEnd + 1;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (char !== '"' && char !== "'" && char !== "`") {
|
||
output += char;
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
output += char;
|
||
index += 1;
|
||
while (index < content.length) {
|
||
const stringChar = content[index];
|
||
output += stringChar;
|
||
index += 1;
|
||
if (stringChar === "\\") {
|
||
if (index < content.length) {
|
||
output += content[index];
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
if (stringChar === char) break;
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function isRegexLiteralStart(content, slashIndex) {
|
||
let cursor = skipWhitespaceBackward(content, slashIndex - 1);
|
||
if (cursor < 0) return true;
|
||
|
||
const previous = content[cursor];
|
||
if ("([{:;,=!?&|+-*%^~<>".includes(previous)) return true;
|
||
return /\b(?:return|throw|case|delete|typeof|void|new|in|of|yield|await)\b/.test(content.slice(Math.max(0, cursor - 12), cursor + 1));
|
||
}
|
||
|
||
function findRegexLiteralEnd(content, slashIndex) {
|
||
let inCharClass = false;
|
||
for (let cursor = slashIndex + 1; cursor < content.length; cursor += 1) {
|
||
const char = content[cursor];
|
||
if (char === "\\") {
|
||
cursor += 1;
|
||
continue;
|
||
}
|
||
if (char === "[") inCharClass = true;
|
||
if (char === "]") inCharClass = false;
|
||
if (char === "/" && !inCharClass) {
|
||
let end = cursor + 1;
|
||
while (end < content.length && /[a-z]/i.test(content[end])) end += 1;
|
||
return end - 1;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function findMatchingParen(content, openParenIndex) {
|
||
let depth = 0;
|
||
for (let index = openParenIndex; index < content.length; index += 1) {
|
||
const char = content[index];
|
||
if (char === "(") depth += 1;
|
||
if (char === ")") {
|
||
depth -= 1;
|
||
if (depth === 0) return index;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function findMatchingBrace(content, openBraceIndex) {
|
||
let depth = 0;
|
||
for (let index = openBraceIndex; index < content.length; index += 1) {
|
||
const char = content[index];
|
||
if (char === "{") depth += 1;
|
||
if (char === "}") {
|
||
depth -= 1;
|
||
if (depth === 0) return index;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function findMatchingBracket(content, openBracketIndex) {
|
||
let depth = 0;
|
||
for (let index = openBracketIndex; index < content.length; index += 1) {
|
||
const char = content[index];
|
||
if (char === "[") depth += 1;
|
||
if (char === "]") {
|
||
depth -= 1;
|
||
if (depth === 0) return index;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function stripParenthesesAroundStaticMemberChains(content, structure) {
|
||
let output = structure;
|
||
let changed = false;
|
||
|
||
do {
|
||
changed = false;
|
||
for (let index = 0; index < output.length; index += 1) {
|
||
if (output[index] !== "(") continue;
|
||
if (!isParenthesizedExpressionOpen(output, index)) continue;
|
||
|
||
const closeParenIndex = findMatchingParen(output, index);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
const innerStart = skipWhitespaceForward(output, index + 1);
|
||
const innerEnd = skipWhitespaceBackward(output, closeParenIndex - 1) + 1;
|
||
if (innerStart >= innerEnd) continue;
|
||
|
||
const memberMatch = simpleStaticMemberChainMatchAt(content, output, innerStart);
|
||
if (!memberMatch || innerStart + memberMatch[0].length !== innerEnd) continue;
|
||
|
||
// Prisma/raw callee 允许完整 member chain 外包多层括号;剥离括号后仍保留源码索引长度。
|
||
output = `${output.slice(0, index)} ${output.slice(index + 1, closeParenIndex)} ${output.slice(closeParenIndex + 1)}`;
|
||
changed = true;
|
||
break;
|
||
}
|
||
} while (changed);
|
||
|
||
return output;
|
||
}
|
||
|
||
function maskStringLiterals(content) {
|
||
let output = "";
|
||
let index = 0;
|
||
|
||
while (index < content.length) {
|
||
const quote = content[index];
|
||
if (quote === "/" && isRegexLiteralStart(content, index)) {
|
||
const literalEnd = findRegexLiteralEnd(content, index);
|
||
if (literalEnd !== -1) {
|
||
output += "/".padEnd(literalEnd - index, " ") + "/";
|
||
index = literalEnd + 1;
|
||
continue;
|
||
}
|
||
}
|
||
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
||
output += quote;
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
output += quote;
|
||
index += 1;
|
||
while (index < content.length) {
|
||
const char = content[index];
|
||
if (char === "\\") {
|
||
output += " ";
|
||
index += 1;
|
||
if (index < content.length) {
|
||
output += " ";
|
||
index += 1;
|
||
}
|
||
continue;
|
||
}
|
||
if (char === quote) {
|
||
output += quote;
|
||
index += 1;
|
||
break;
|
||
}
|
||
output += " ";
|
||
index += 1;
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function findStringLiteralEnd(content, literalStart) {
|
||
const quote = content[literalStart];
|
||
let cursor = literalStart + 1;
|
||
|
||
while (cursor < content.length) {
|
||
const char = content[cursor];
|
||
if (char === "\\") {
|
||
cursor += 2;
|
||
continue;
|
||
}
|
||
if (char === quote) return cursor;
|
||
cursor += 1;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
function hasPrismaCallContaining(content, callPattern, termPattern) {
|
||
// 结构匹配只看源码骨架;注释中的括号/花括号不能参与调用体边界计算。
|
||
const scanContent = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
for (const match of scanContent.matchAll(callPattern)) {
|
||
const openParenIndex = scanContent.indexOf("(", match.index);
|
||
if (openParenIndex === -1) continue;
|
||
const closeParenIndex = findMatchingParen(scanContent, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1);
|
||
if (termPattern.test(callBody)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function hasPrismaCallBodyContaining(scanContent, callStart, termPattern) {
|
||
const openParenIndex = scanContent[callStart] === "(" ? callStart : scanContent.indexOf("(", callStart);
|
||
if (openParenIndex === -1) return false;
|
||
|
||
const closeParenIndex = findMatchingParen(scanContent, openParenIndex);
|
||
if (closeParenIndex === -1) return false;
|
||
|
||
const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1);
|
||
return termPattern.test(callBody);
|
||
}
|
||
|
||
function hasPrismaModelMethodCallContaining(content, models, methods, termPattern) {
|
||
// delegate alias 与直接 model access 共用同一调用体解析,避免 Job/Audit/guarded 边界分叉。
|
||
const scanContent = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
for (const match of prismaModelMethodCallMatchesInStructure(content, scanContent, models, methods)) {
|
||
if (hasPrismaCallBodyContaining(scanContent, match.callStart, termPattern)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function hasVariableizedOrSpreadJobData(callBody) {
|
||
return (
|
||
/\bdata\s*(?:[,}])/.test(callBody) ||
|
||
/\bdata\s*:\s*[A-Za-z_$][\w$]*\b/.test(callBody) ||
|
||
/\bdata\s*:\s*(?:\{|\[)[\s\S]*\.\.\./.test(callBody)
|
||
);
|
||
}
|
||
|
||
function hasComputedJobDataKey(callBody) {
|
||
const dataObjectPattern = /\bdata\s*:\s*\{/g;
|
||
for (const match of callBody.matchAll(dataObjectPattern)) {
|
||
const openBraceIndex = callBody.indexOf("{", match.index);
|
||
if (openBraceIndex === -1) continue;
|
||
const closeBraceIndex = findMatchingBrace(callBody, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
const dataObjectBody = callBody.slice(openBraceIndex, closeBraceIndex + 1);
|
||
// Job 运行态字段必须由 JobExecutionStore 单写;computed key 无法静态证明安全,按违规处理。
|
||
if (/\[[^\]]+\]\s*:/.test(dataObjectBody)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function hasForbiddenJobWriteCallBody(callBody) {
|
||
const runtimeFieldPattern = new RegExp(`\\b(?:${jobRuntimeFields.map(escapeRegExp).join("|")})\\b`);
|
||
return runtimeFieldPattern.test(callBody) || hasVariableizedOrSpreadJobData(callBody) || hasComputedJobDataKey(callBody);
|
||
}
|
||
|
||
function hasRawJobRuntimeFieldWrite(sqlSource) {
|
||
const sql = normalizeRawSqlScanSource(sqlSource);
|
||
const jobTable = sqlTableReferencePattern("Job");
|
||
const identifier = sqlBareIdentifierPattern();
|
||
const runtimeField = String.raw`(?:${jobRuntimeFields.map(escapeRegExp).join("|")})`;
|
||
const maybeQualifiedRuntimeField = String.raw`(?:${identifier}\s*\.\s*)?${runtimeField}`;
|
||
// PostgreSQL UPDATE 允许目标表后带 `*` 继承标记;扫描时必须先吃掉它再匹配 alias/SET。
|
||
const updateJobRuntimeFieldPattern = new RegExp(
|
||
String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b[\s\S]*?\b${maybeQualifiedRuntimeField}\s*=`,
|
||
"i"
|
||
);
|
||
const updateJobTablePattern = new RegExp(
|
||
String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`,
|
||
"i"
|
||
);
|
||
const mergeJobTablePattern = new RegExp(
|
||
String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${jobTable}(?:\s+(?:AS\s+)?${identifier})?\b`,
|
||
"i"
|
||
);
|
||
|
||
return (
|
||
updateJobRuntimeFieldPattern.test(sql) ||
|
||
updateJobTablePattern.test(sql) ||
|
||
mergeJobTablePattern.test(sql) ||
|
||
new RegExp(String.raw`\bINSERT\s+INTO\s+${jobTable}\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${jobTable}\b`, "i").test(sql) ||
|
||
new RegExp(sqlTruncateTableListPattern(jobTable), "i").test(sql)
|
||
);
|
||
}
|
||
|
||
function findStatementEnd(structure, startIndex) {
|
||
const semicolonIndex = structure.indexOf(";", startIndex);
|
||
return semicolonIndex === -1 ? structure.length : semicolonIndex;
|
||
}
|
||
|
||
function findVariableInitializerSources(content, structure, variableName, beforeIndex) {
|
||
const sources = [];
|
||
const escapedName = escapeRegExp(variableName);
|
||
const declarationPattern = new RegExp(
|
||
String.raw`\b(?:const|let|var)\s+${escapedName}\b(?:\s*:[^=;]+)?\s*=|\b${escapedName}\s*=`,
|
||
"g"
|
||
);
|
||
|
||
for (const match of structure.matchAll(declarationPattern)) {
|
||
if (typeof match.index !== "number" || match.index >= beforeIndex) continue;
|
||
const equalsIndex = structure.indexOf("=", match.index);
|
||
if (equalsIndex === -1 || equalsIndex >= beforeIndex) continue;
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const initializerEnd = findStatementEnd(structure, initializerStart);
|
||
sources.push({ index: initializerStart, end: initializerEnd, source: content.slice(initializerStart, initializerEnd) });
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function findLatestVariableInitializerSource(content, structure, variableName, beforeIndex) {
|
||
const sources = findVariableInitializerSources(content, structure, variableName, beforeIndex);
|
||
return sources.length === 0 ? null : sources.at(-1);
|
||
}
|
||
|
||
function isStaticStringLiteralSource(source) {
|
||
const trimmed = source.trim();
|
||
if (trimmed.length < 2) return false;
|
||
|
||
const quote = trimmed[0];
|
||
if (quote !== '"' && quote !== "'" && quote !== "`") return false;
|
||
const literalEnd = findStringLiteralEnd(trimmed, 0);
|
||
if (literalEnd !== trimmed.length - 1) return false;
|
||
|
||
// 带表达式的 template literal 不是静态 SQL 片段,不能当成确定表名/字段名展开。
|
||
return quote !== "`" || !/\$\{/.test(trimmed);
|
||
}
|
||
|
||
function isStaticPrismaRawExpressionSource(source) {
|
||
const trimmed = source.trim();
|
||
const rawCallPattern = /\bPrisma\s*\.\s*raw\s*\(/;
|
||
const rawCallMatch = rawCallPattern.exec(trimmed);
|
||
if (!rawCallMatch || rawCallMatch.index !== 0) return false;
|
||
|
||
const openParenIndex = trimmed.indexOf("(", rawCallMatch.index + rawCallMatch[0].length - 1);
|
||
if (openParenIndex === -1) return false;
|
||
const closeParenIndex = findMatchingParen(maskCommentsOutsideStringLiterals(trimmed), openParenIndex);
|
||
if (closeParenIndex !== trimmed.length - 1) return false;
|
||
|
||
return isStaticStringLiteralSource(trimmed.slice(openParenIndex + 1, closeParenIndex));
|
||
}
|
||
|
||
function isStaticSqlFragmentSource(source) {
|
||
return isStaticStringLiteralSource(source) || isStaticPrismaRawExpressionSource(source);
|
||
}
|
||
|
||
function applyTextReplacements(source, replacements) {
|
||
let output = source;
|
||
for (const replacement of [...replacements].sort((left, right) => right.start - left.start)) {
|
||
output = `${output.slice(0, replacement.start)}${replacement.value}${output.slice(replacement.end)}`;
|
||
}
|
||
return output;
|
||
}
|
||
|
||
function staticSqlFragmentReplacementSource(content, structure, variableName, beforeIndex, seenVariables, depth) {
|
||
if (depth >= rawSqlVariableResolutionMaxDepth || seenVariables.has(variableName)) return null;
|
||
|
||
const initializer = findLatestVariableInitializerSource(content, structure, variableName, beforeIndex);
|
||
if (!initializer) return null;
|
||
|
||
const nestedSeenVariables = new Set(seenVariables);
|
||
nestedSeenVariables.add(variableName);
|
||
const expandedSource = expandStaticSqlSource(
|
||
content,
|
||
structure,
|
||
initializer.source,
|
||
initializer.index,
|
||
initializer.end,
|
||
initializer.index,
|
||
nestedSeenVariables,
|
||
depth + 1
|
||
);
|
||
|
||
return isStaticSqlFragmentSource(expandedSource) ? expandedSource.trim() : null;
|
||
}
|
||
|
||
function expandIdentifiersOutsideStringLiterals(content, structure, source, sourceStart, sourceEnd, seenVariables, depth) {
|
||
const sourceStructure = structure.slice(sourceStart, sourceEnd);
|
||
const replacements = [];
|
||
const identifierPattern = /\b[A-Za-z_$][\w$]*\b/g;
|
||
|
||
for (const match of sourceStructure.matchAll(identifierPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const variableName = match[0];
|
||
const replacement = staticSqlFragmentReplacementSource(
|
||
content,
|
||
structure,
|
||
variableName,
|
||
sourceStart + match.index,
|
||
seenVariables,
|
||
depth
|
||
);
|
||
if (!replacement) continue;
|
||
|
||
replacements.push({
|
||
start: match.index,
|
||
end: match.index + variableName.length,
|
||
value: replacement
|
||
});
|
||
}
|
||
|
||
return applyTextReplacements(source, replacements);
|
||
}
|
||
|
||
function expandPrismaRawIdentifierArguments(content, structure, source, sourceStart, seenVariables, depth) {
|
||
return source.replace(/\bPrisma\s*\.\s*raw\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g, (match, variableName, offset) => {
|
||
const variableOffset = offset + match.lastIndexOf(variableName);
|
||
const replacement = staticSqlFragmentReplacementSource(
|
||
content,
|
||
structure,
|
||
variableName,
|
||
sourceStart + variableOffset,
|
||
seenVariables,
|
||
depth
|
||
);
|
||
|
||
return replacement ? match.replace(variableName, replacement) : match;
|
||
});
|
||
}
|
||
|
||
function expandStaticPrismaRawTemplateIdentifiers(content, structure, source, sourceStart, seenVariables, depth) {
|
||
return source.replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (match, variableName, offset) => {
|
||
const variableOffset = offset + match.lastIndexOf(variableName);
|
||
const replacement = staticSqlFragmentReplacementSource(
|
||
content,
|
||
structure,
|
||
variableName,
|
||
sourceStart + variableOffset,
|
||
seenVariables,
|
||
depth
|
||
);
|
||
|
||
// 只有已证明是 Prisma.raw(...) 的片段才当作 SQL 文本;普通 template 变量仍是参数。
|
||
return replacement && isStaticPrismaRawExpressionSource(replacement) ? `\${${replacement}}` : match;
|
||
});
|
||
}
|
||
|
||
function expandStaticSqlSource(content, structure, source, sourceStart, sourceEnd, beforeIndex, seenVariables = new Set(), depth = 0) {
|
||
if (depth >= rawSqlVariableResolutionMaxDepth) return source;
|
||
|
||
// 先展开字符串拼接里的静态中间变量,再补齐 Prisma.sql 模板中 Prisma.raw(...) 的静态参数。
|
||
const withConcatenatedIdentifiers = expandIdentifiersOutsideStringLiterals(
|
||
content,
|
||
structure,
|
||
source,
|
||
sourceStart,
|
||
sourceEnd,
|
||
seenVariables,
|
||
depth
|
||
);
|
||
const withRawArguments = expandPrismaRawIdentifierArguments(
|
||
content,
|
||
structure,
|
||
withConcatenatedIdentifiers,
|
||
sourceStart,
|
||
seenVariables,
|
||
depth
|
||
);
|
||
return expandStaticPrismaRawTemplateIdentifiers(content, structure, withRawArguments, sourceStart, seenVariables, depth);
|
||
}
|
||
|
||
function markDynamicTemplateSqlExpressions(source) {
|
||
return source.replace(/\$\{[\s\S]*?\}/g, (match) => {
|
||
const expressionSource = match.slice(2, -1).trim();
|
||
// Prisma.raw 静态字面量已可证明为 identifier;其他模板表达式在 SQL 结构位按动态片段处理。
|
||
return isStaticPrismaRawExpressionSource(expressionSource) ? match : ` ${dynamicSqlIdentifierMarker} `;
|
||
});
|
||
}
|
||
|
||
function markDynamicCallExpressionsOutsideStringLiterals(source) {
|
||
const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(source));
|
||
const replacements = [];
|
||
const callPattern = /\b[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*\s*\(/g;
|
||
const skippedCalls = new Set(["Prisma.raw", "Prisma.sql"]);
|
||
|
||
for (const match of structure.matchAll(callPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const openParenIndex = structure.indexOf("(", match.index + match[0].length - 1);
|
||
if (openParenIndex === -1) continue;
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
const callName = match[0]
|
||
.slice(0, match[0].lastIndexOf("("))
|
||
.replace(/\s+/g, "");
|
||
if (skippedCalls.has(callName)) continue;
|
||
|
||
// 写 SQL 结构位的函数调用无法静态证明目标表/字段,统一当动态 identifier。
|
||
replacements.push({
|
||
start: match.index,
|
||
end: closeParenIndex + 1,
|
||
value: dynamicSqlIdentifierMarker
|
||
});
|
||
}
|
||
|
||
return applyTextReplacements(source, replacements);
|
||
}
|
||
|
||
function markDynamicIdentifiersOutsideStringLiterals(source) {
|
||
const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(source));
|
||
const replacements = [];
|
||
const skippedIdentifiers = new Set(["Prisma", "sql", "raw", "true", "false", "null", "undefined"]);
|
||
const identifierPattern = /\b[A-Za-z_$][\w$]*\b/g;
|
||
|
||
for (const match of structure.matchAll(identifierPattern)) {
|
||
if (typeof match.index !== "number" || skippedIdentifiers.has(match[0])) continue;
|
||
replacements.push({
|
||
start: match.index,
|
||
end: match.index + match[0].length,
|
||
value: dynamicSqlIdentifierMarker
|
||
});
|
||
}
|
||
|
||
return applyTextReplacements(source, replacements);
|
||
}
|
||
|
||
function markDynamicSqlIdentifierFragments(source) {
|
||
return markDynamicIdentifiersOutsideStringLiterals(markDynamicCallExpressionsOutsideStringLiterals(markDynamicTemplateSqlExpressions(source)));
|
||
}
|
||
|
||
function rawSqlScanCandidateSources(content, structure, source, sourceStart, sourceEnd, beforeIndex) {
|
||
const expandedSource = expandStaticSqlSource(content, structure, source, sourceStart, sourceEnd, beforeIndex);
|
||
const dynamicMarkedSource = markDynamicSqlIdentifierFragments(expandedSource);
|
||
return [...new Set([source, expandedSource, dynamicMarkedSource])];
|
||
}
|
||
|
||
function firstCallArgumentSource(content, structure, openParenIndex, closeParenIndex) {
|
||
const argumentStart = skipWhitespaceForward(structure, openParenIndex + 1);
|
||
if (argumentStart >= closeParenIndex) return null;
|
||
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
|
||
for (let cursor = argumentStart; cursor < closeParenIndex; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (char === "," && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
||
return {
|
||
index: argumentStart,
|
||
end: cursor,
|
||
source: content.slice(argumentStart, cursor)
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
index: argumentStart,
|
||
end: closeParenIndex,
|
||
source: content.slice(argumentStart, closeParenIndex)
|
||
};
|
||
}
|
||
|
||
function hasDynamicRawSqlWrite(sqlSource) {
|
||
const sql = normalizeRawSqlScanSource(sqlSource);
|
||
const dynamicIdentifier = escapeRegExp(dynamicSqlIdentifierMarker);
|
||
const identifier = sqlBareIdentifierPattern();
|
||
const triggerIdentifier = sqlTriggerIdentifierPattern();
|
||
const dynamicTableReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})`;
|
||
const dynamicFieldReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})`;
|
||
const dynamicFunctionReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})(?:\s*\([^)]*\))?`;
|
||
const dynamicFunctionList = sqlFunctionListContainingPattern(dynamicFunctionReference);
|
||
const anyTableReference = sqlAnyTableReferencePattern();
|
||
|
||
// raw 写 SQL 里的动态表/字段/trigger identifier 无法静态证明边界,必须 fail-closed。
|
||
return (
|
||
new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${dynamicTableReference}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(String.raw`\bUPDATE\b[\s\S]*?\bSET\b[\s\S]*?\b${dynamicFieldReference}\s*=`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bINSERT\s+INTO\s+${dynamicTableReference}\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bINSERT\s+INTO\b[\s\S]*?\([^)]*\b${dynamicFieldReference}\b[^)]*\)\s+VALUES\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${dynamicTableReference}(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${dynamicTableReference}\b`, "i").test(sql) ||
|
||
new RegExp(sqlTruncateTableListPattern(dynamicTableReference), "i").test(sql) ||
|
||
new RegExp(sqlDropTableListPattern(dynamicTableReference), "i").test(sql) ||
|
||
new RegExp(String.raw`\bDROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?${dynamicFieldReference}\s+ON\s+${anyTableReference}\b`, "i").test(
|
||
sql
|
||
) ||
|
||
new RegExp(
|
||
String.raw`\bDROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?${triggerIdentifier}\s+ON\s+(?:ONLY\s+)?${dynamicTableReference}\b`,
|
||
"i"
|
||
).test(sql) ||
|
||
new RegExp(String.raw`\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?${dynamicTableReference}\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bALTER\s+TABLE\b[\s\S]*?\bDISABLE\s+TRIGGER\s+${dynamicFieldReference}\b`, "i").test(sql) ||
|
||
// 函数 DDL 可以替换/移除 AuditLog append-only guard,动态函数名必须 fail-closed。
|
||
new RegExp(String.raw`\bDROP\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${dynamicFunctionList}(?=\s|;|$)`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bALTER\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${dynamicFunctionReference}(?=\s|;|$)`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+${dynamicFunctionReference}(?=\s|,|;|$)`, "i").test(sql)
|
||
);
|
||
}
|
||
|
||
function hasDynamicRawSqlWriteCandidate(content, structure, source, sourceStart, sourceEnd, beforeIndex) {
|
||
return rawSqlScanCandidateSources(content, structure, source, sourceStart, sourceEnd, beforeIndex).some((candidateSource) =>
|
||
hasDynamicRawSqlWrite(candidateSource)
|
||
);
|
||
}
|
||
|
||
function firstCallIdentifierArgument(structure, openParenIndex, closeParenIndex) {
|
||
const argumentStart = skipWhitespaceForward(structure, openParenIndex + 1);
|
||
if (argumentStart >= closeParenIndex) return null;
|
||
|
||
const match = /^[A-Za-z_$][\w$]*/.exec(structure.slice(argumentStart, closeParenIndex));
|
||
if (!match) return null;
|
||
const identifierEnd = argumentStart + match[0].length;
|
||
const nextIndex = skipWhitespaceForward(structure, identifierEnd);
|
||
|
||
// 只跟踪 `raw(query)` 或 `raw(query, ...)` 这种简单变量,避免误读属性访问或函数调用。
|
||
if (structure[nextIndex] !== "," && nextIndex !== closeParenIndex) return null;
|
||
return match[0];
|
||
}
|
||
|
||
function nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, argumentIndex) {
|
||
const argumentStart = skipWhitespaceForward(structure, openParenIndex + 1);
|
||
if (argumentStart >= closeParenIndex) return null;
|
||
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
let currentArgumentIndex = 0;
|
||
let currentArgumentStart = argumentStart;
|
||
|
||
for (let cursor = argumentStart; cursor < closeParenIndex; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (char === "," && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
||
if (currentArgumentIndex === argumentIndex) {
|
||
const end = skipWhitespaceBackward(structure, cursor - 1) + 1;
|
||
return { index: currentArgumentStart, end, source: content.slice(currentArgumentStart, end) };
|
||
}
|
||
|
||
currentArgumentIndex += 1;
|
||
currentArgumentStart = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
}
|
||
|
||
if (currentArgumentIndex !== argumentIndex || currentArgumentStart >= closeParenIndex) return null;
|
||
const end = skipWhitespaceBackward(structure, closeParenIndex - 1) + 1;
|
||
return { index: currentArgumentStart, end, source: content.slice(currentArgumentStart, end) };
|
||
}
|
||
|
||
const rawSqlExecutorNames = ["$executeRawUnsafe", "$executeRaw", "$queryRawUnsafe", "$queryRaw"];
|
||
const rawSqlExecutorPatternSource =
|
||
String.raw`${staticPrismaPropertyAccessPattern(rawSqlExecutorNames)}\s*(?:<[^>()` + "`" + String.raw`]*>)?\s*`;
|
||
|
||
function rawSqlExecutorPattern() {
|
||
return new RegExp(rawSqlExecutorPatternSource, "g");
|
||
}
|
||
|
||
function rawSqlStructure(content) {
|
||
// raw executor 需要保留 db["$executeRawUnsafe"] 这类静态 bracket key;SQL 参数本体仍等长遮蔽。
|
||
return stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
}
|
||
|
||
function findOpeningParen(content, closeParenIndex) {
|
||
let depth = 0;
|
||
for (let index = closeParenIndex; index >= 0; index -= 1) {
|
||
const char = content[index];
|
||
if (char === ")") depth += 1;
|
||
if (char === "(") {
|
||
depth -= 1;
|
||
if (depth === 0) return index;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function isParenthesizedExpressionOpen(structure, openParenIndex) {
|
||
const previousIndex = skipWhitespaceBackward(structure, openParenIndex - 1);
|
||
if (previousIndex < 0) return true;
|
||
|
||
const previous = structure[previousIndex];
|
||
if ("([{=,:;!?&|+-*%^~<>".includes(previous)) return true;
|
||
|
||
if (!/[\w$]/.test(previous)) return false;
|
||
|
||
const previousWord = /[A-Za-z_$][\w$]*$/.exec(structure.slice(0, previousIndex + 1))?.[0] ?? "";
|
||
return /^(?:return|throw|await|yield|case|delete|typeof|void|new|in|of)$/.test(previousWord);
|
||
}
|
||
|
||
function skipOptionalTypeArguments(structure, index) {
|
||
let cursor = skipWhitespaceForward(structure, index);
|
||
if (structure.startsWith("?.", cursor)) {
|
||
const optionalCursor = skipWhitespaceForward(structure, cursor + 2);
|
||
if (structure[optionalCursor] !== "<") return cursor;
|
||
|
||
const optionalTypeEnd = staticTypeArgumentsEnd(structure, optionalCursor);
|
||
if (optionalTypeEnd === -1) return cursor;
|
||
|
||
const afterOptionalType = skipWhitespaceForward(structure, optionalTypeEnd);
|
||
return structure[afterOptionalType] === "(" ? afterOptionalType : cursor;
|
||
}
|
||
|
||
if (structure[cursor] !== "<") return cursor;
|
||
|
||
const typeEnd = staticTypeArgumentsEnd(structure, cursor);
|
||
if (typeEnd === -1) return cursor;
|
||
|
||
const afterType = skipWhitespaceForward(structure, typeEnd);
|
||
return structure[afterType] === "(" ? afterType : cursor;
|
||
}
|
||
|
||
function staticTypeArgumentsEnd(structure, openAngleIndex) {
|
||
if (structure[openAngleIndex] !== "<") return -1;
|
||
|
||
let depth = 1;
|
||
let sawTypeToken = false;
|
||
|
||
for (let cursor = openAngleIndex + 1; cursor < structure.length; cursor += 1) {
|
||
const char = structure[cursor];
|
||
|
||
if (char === '"' || char === "'" || char === "`") {
|
||
const literalEnd = findStringLiteralEnd(structure, cursor);
|
||
if (literalEnd === -1) return -1;
|
||
cursor = literalEnd;
|
||
sawTypeToken = true;
|
||
continue;
|
||
}
|
||
|
||
if (char === "<") {
|
||
depth += 1;
|
||
sawTypeToken = true;
|
||
continue;
|
||
}
|
||
|
||
if (char === ">") {
|
||
depth -= 1;
|
||
if (depth === 0) return sawTypeToken ? cursor + 1 : -1;
|
||
continue;
|
||
}
|
||
|
||
if (char === "(" || char === ")" || char === "{" || char === "}" || char === ";" || char === "\n" || char === "\r") {
|
||
return -1;
|
||
}
|
||
|
||
if (!/\s/.test(char)) sawTypeToken = true;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
function parenthesizedRawSqlOperationStart(structure, rawAccessEnd, closeParenIndex) {
|
||
let cursor = closeParenIndex;
|
||
|
||
while (structure[cursor] === ")") {
|
||
const openParenIndex = findOpeningParen(structure, cursor);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) return -1;
|
||
if (rawAccessEnd > cursor) return -1;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
return cursor;
|
||
}
|
||
|
||
function rawSqlMemberOperation(content, structure, callStart) {
|
||
const operationStart = skipWhitespaceForward(structure, callStart);
|
||
const genericCallStart = skipOptionalTypeArguments(structure, operationStart);
|
||
if (genericCallStart !== operationStart && structure[genericCallStart] === "(") {
|
||
// raw executor 的 TS 泛型调用 `raw<T>(...)` / `raw?.<T>(...)` 与普通调用等价,SQL 参数仍从调用括号读取。
|
||
return { kind: "direct", callStart: genericCallStart };
|
||
}
|
||
|
||
if (structure[operationStart] === "(") {
|
||
return { kind: "direct", callStart: operationStart };
|
||
}
|
||
|
||
if (structure[operationStart] === "`") {
|
||
return { kind: "template", templateStart: operationStart };
|
||
}
|
||
|
||
if (structure[operationStart] === ")") {
|
||
// raw executor 被括号包成 callee 时,真实调用操作在 closing paren 之后。
|
||
const parenthesizedOperationStart = parenthesizedRawSqlOperationStart(structure, callStart, operationStart);
|
||
if (parenthesizedOperationStart !== -1) {
|
||
return rawSqlMemberOperation(content, structure, parenthesizedOperationStart);
|
||
}
|
||
}
|
||
|
||
if (structure.startsWith("?.", operationStart)) {
|
||
const optionalTargetStart = skipWhitespaceForward(structure, operationStart + 2);
|
||
if (structure[optionalTargetStart] === "(") {
|
||
return { kind: "direct", callStart: optionalTargetStart };
|
||
}
|
||
|
||
const optionalOperation = staticCallableMemberOperationMatchAt(content, structure, operationStart, ["call", "bind", "apply"]);
|
||
if (optionalOperation) return optionalOperation;
|
||
}
|
||
|
||
return staticCallableMemberOperationMatchAt(content, structure, operationStart, ["call", "bind", "apply"]);
|
||
}
|
||
|
||
function rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd = structure.length) {
|
||
const operation = rawSqlMemberOperation(content, structure, rawMatchEnd);
|
||
if (operation?.kind !== "bind") return -1;
|
||
|
||
const methodOpenParenIndex = operation.openParenIndex ?? structure.indexOf("(", operation.methodNameEnd);
|
||
if (methodOpenParenIndex === -1 || methodOpenParenIndex >= statementEnd) return -1;
|
||
|
||
const methodCloseParenIndex = operation.closeParenIndex ?? findMatchingParen(structure, methodOpenParenIndex);
|
||
return methodCloseParenIndex !== -1 && methodCloseParenIndex <= statementEnd ? methodCloseParenIndex : -1;
|
||
}
|
||
|
||
function hasLineTerminator(source) {
|
||
return /[\n\r\u2028\u2029]/.test(source);
|
||
}
|
||
|
||
function isRawSqlBoundAliasExpressionContinuation(structure, index) {
|
||
if (index >= structure.length) return false;
|
||
if (structure.startsWith("?.", index)) return true;
|
||
|
||
// 换行后这些 token 仍可能继续前一条表达式;此时不能把 `.bind(...)` 当作 ASI 结束。
|
||
return "([.`+-/*%,".includes(structure[index]);
|
||
}
|
||
|
||
function rawSqlBoundAliasInitializerEnd(structure, bindCloseParenIndex, statementEnd) {
|
||
const nextTokenIndex = skipWhitespaceForward(structure, bindCloseParenIndex + 1);
|
||
if (nextTokenIndex === statementEnd) return statementEnd;
|
||
|
||
const gapAfterBind = structure.slice(bindCloseParenIndex + 1, nextTokenIndex);
|
||
if (!hasLineTerminator(gapAfterBind)) return -1;
|
||
if (isRawSqlBoundAliasExpressionContinuation(structure, nextTokenIndex)) return -1;
|
||
|
||
// 无分号合法代码依靠 ASI 在换行处结束 initializer;alias.end 只覆盖 `.bind(...)` 表达式本身。
|
||
return bindCloseParenIndex + 1;
|
||
}
|
||
|
||
function isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, rawMatchStart) {
|
||
const prefix = structure.slice(initializerStart, rawMatchStart).trim();
|
||
if (prefix.length === 0) return false;
|
||
|
||
// 防止无分号场景把后续语句里的 raw executor 误归到当前声明 initializer。
|
||
return !/[;\n\r\u2028\u2029]/.test(prefix) && !/\b(?:const|let|var|await|return|throw|if|for|while|switch|class|function|import|export)\b/.test(prefix);
|
||
}
|
||
|
||
function isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart) {
|
||
const prefix = structure.slice(initializerStart, rawMatchStart).trim();
|
||
if (prefix.length === 0) return false;
|
||
|
||
// 普通 raw executor alias 只接受静态 member chain,避免把条件表达式或函数返回值误判成 Prisma raw executor。
|
||
const staticMemberChain = String.raw`(?:[A-Za-z_$][\w$]*|this)(?:\s*(?:(?:\.|\?\.)\s*[A-Za-z_$][\w$]*|(?:\?\.\s*)?\[\s*(?:"[^"]*"|'[^']*'|` + "`[^`]*`" + String.raw`)\s*\]))*`;
|
||
return new RegExp(String.raw`^${staticMemberChain}$`).test(prefix);
|
||
}
|
||
|
||
function rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd) {
|
||
const nextTokenIndex = skipWhitespaceForward(structure, rawMatchEnd);
|
||
if (nextTokenIndex === statementEnd) return statementEnd;
|
||
|
||
const gapAfterAccess = structure.slice(rawMatchEnd, nextTokenIndex);
|
||
if (!hasLineTerminator(gapAfterAccess)) return -1;
|
||
if (isRawSqlBoundAliasExpressionContinuation(structure, nextTokenIndex)) return -1;
|
||
|
||
// 无分号合法代码依靠 ASI 在换行处结束 initializer;alias.end 只覆盖 raw executor access 本身。
|
||
return rawMatchEnd;
|
||
}
|
||
|
||
function simpleRawSqlAliasInitializerEnd(structure, expressionEnd, statementEnd) {
|
||
const nextTokenIndex = skipWhitespaceForward(structure, expressionEnd);
|
||
if (nextTokenIndex === statementEnd) return statementEnd;
|
||
|
||
const gapAfterExpression = structure.slice(expressionEnd, nextTokenIndex);
|
||
if (!hasLineTerminator(gapAfterExpression)) return -1;
|
||
if (isRawSqlBoundAliasExpressionContinuation(structure, nextTokenIndex)) return -1;
|
||
|
||
return expressionEnd;
|
||
}
|
||
|
||
function simpleStaticMemberChainMatchAt(content, structure, index) {
|
||
const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(index));
|
||
if (!receiverMatch) return null;
|
||
|
||
let cursor = index + receiverMatch[0].length;
|
||
while (true) {
|
||
const nextCursor = skipWhitespaceForward(structure, cursor);
|
||
if (structure[nextCursor] !== "." && structure[nextCursor] !== "[" && !structure.startsWith("?.", nextCursor)) break;
|
||
|
||
const propertyAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, nextCursor);
|
||
if (!propertyAccess) break;
|
||
cursor = propertyAccess.end;
|
||
}
|
||
|
||
return [structure.slice(index, cursor)];
|
||
}
|
||
|
||
function staticStaticMemberPropertyAccessMatchAt(content, structure, index) {
|
||
let propertyStart = index;
|
||
if (structure.startsWith("?.", index)) {
|
||
propertyStart = skipWhitespaceForward(structure, index + 2);
|
||
} else if (structure[index] === ".") {
|
||
propertyStart = skipWhitespaceForward(structure, index + 1);
|
||
}
|
||
|
||
if (structure[propertyStart] === "[") {
|
||
const closeBracketIndex = findMatchingBracket(structure, propertyStart);
|
||
if (closeBracketIndex === -1) return null;
|
||
|
||
const expressionStart = propertyStart + 1;
|
||
const expressionEnd = closeBracketIndex;
|
||
const values = staticJavaScriptExpressionValues(
|
||
content,
|
||
structure,
|
||
content.slice(expressionStart, expressionEnd),
|
||
expressionStart,
|
||
index,
|
||
new Set(),
|
||
0
|
||
);
|
||
return values.length === 1 ? { index, end: closeBracketIndex + 1, value: values[0] } : null;
|
||
}
|
||
|
||
if (propertyStart === index && structure[index] !== "." && !structure.startsWith("?.", index)) return null;
|
||
|
||
const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(propertyStart));
|
||
return nameMatch ? { index, end: propertyStart + nameMatch[0].length, value: nameMatch[0] } : null;
|
||
}
|
||
|
||
function rawSqlExecutorAccessMatches(content, structure, startIndex = 0, endIndex = structure.length) {
|
||
const matches = [];
|
||
|
||
for (let cursor = startIndex; cursor < endIndex; cursor += 1) {
|
||
if (structure[cursor] !== "." && structure[cursor] !== "[" && !structure.startsWith("?.", cursor)) continue;
|
||
|
||
const match = staticPrismaPropertyAccessMatchAt(content, structure, cursor, rawSqlExecutorNames, cursor);
|
||
if (!match || match.end > endIndex) continue;
|
||
|
||
matches.push(match);
|
||
cursor = match.end - 1;
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
function completeRawSqlBoundExecutorAliasExpressionMatch(content, structure, sourceStart, sourceEnd) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, expressionStart, expressionEnd)) {
|
||
if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, expressionStart, rawMatch.index)) continue;
|
||
|
||
const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatch.end, expressionEnd);
|
||
if (bindCloseParenIndex === -1) continue;
|
||
if (!isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, expressionEnd)) continue;
|
||
|
||
return { kind: "rawExecutor", index: rawMatch.index, end: expressionEnd };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function completeRawSqlPlainExecutorAliasExpressionMatch(content, structure, sourceStart, sourceEnd) {
|
||
const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd);
|
||
const expressionStart = skipWhitespaceForward(structure, unwrapped.start);
|
||
const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1;
|
||
if (expressionStart >= expressionEnd) return null;
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, expressionStart, expressionEnd)) {
|
||
if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, expressionStart, rawMatch.index)) continue;
|
||
if (!isAtStaticWrapperExpressionEnd(structure, rawMatch.end, expressionEnd)) continue;
|
||
|
||
return { kind: "rawExecutor", index: rawMatch.index, end: expressionEnd };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function hasRawSqlExecutorAccess(content, structure) {
|
||
return rawSqlExecutorAccessMatches(content, structure).length > 0;
|
||
}
|
||
|
||
function hasRawSqlExecutorAccessInRange(content, structure, startIndex, endIndex) {
|
||
const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure);
|
||
return rawSqlExecutorAccessMatches(content, scanStructure, startIndex, endIndex).length > 0;
|
||
}
|
||
|
||
function findRawSqlBoundExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchRawMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
completeRawSqlBoundExecutorAliasExpressionMatch
|
||
);
|
||
if (branchRawMatch?.branch) {
|
||
// mixed ternary/logical 中任一分支返回 raw executor bind,都把 alias 视为 raw executor,防止 safe 分支掩盖危险分支。
|
||
aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) {
|
||
const rawMatchStart = rawMatch.index;
|
||
if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue;
|
||
|
||
const rawMatchEnd = rawMatch.end;
|
||
const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd);
|
||
if (bindCloseParenIndex === -1) continue;
|
||
|
||
const initializerEnd = rawSqlBoundAliasInitializerEnd(structure, bindCloseParenIndex, statementEnd);
|
||
if (initializerEnd === -1) continue;
|
||
|
||
// 只跟踪同文件的简单 bound executor alias;复杂重赋值/跨函数数据流继续交给后续 review 扩展。
|
||
aliases.push({
|
||
name: declarationMatch[1],
|
||
index: declarationMatch.index,
|
||
end: initializerEnd
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findRawSqlAssignedBoundExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchRawMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
completeRawSqlBoundExecutorAliasExpressionMatch
|
||
);
|
||
if (branchRawMatch?.branch) {
|
||
// 后续赋值也按同一规则 fail-closed,避免 `alias = ok ? raw.bind(db) : safe` 绕过。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) {
|
||
const rawMatchStart = rawMatch.index;
|
||
if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue;
|
||
|
||
const rawMatchEnd = rawMatch.end;
|
||
const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd);
|
||
if (bindCloseParenIndex === -1) continue;
|
||
|
||
const initializerEnd = rawSqlBoundAliasInitializerEnd(structure, bindCloseParenIndex, statementEnd);
|
||
if (initializerEnd === -1) continue;
|
||
|
||
// 赋值 alias 覆盖 `let execRaw; execRaw = db.$executeRawUnsafe.bind(db)` 这类后续绑定。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: initializerEnd });
|
||
break;
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findRawSqlPlainExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", declarationMatch.index);
|
||
if (equalsIndex === -1) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchRawMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
completeRawSqlPlainExecutorAliasExpressionMatch
|
||
);
|
||
if (branchRawMatch?.branch) {
|
||
// 普通 raw executor alias 同样不能被 `safe || db.$executeRawUnsafe` 这类逻辑分支隐藏。
|
||
aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) {
|
||
const rawMatchStart = rawMatch.index;
|
||
if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue;
|
||
|
||
const rawMatchEnd = rawMatch.end;
|
||
const initializerEnd = rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd);
|
||
if (initializerEnd === -1) continue;
|
||
|
||
// 只跟踪同文件的简单 raw executor alias;复杂重赋值/跨函数数据流继续交给后续 review 扩展。
|
||
aliases.push({
|
||
name: declarationMatch[1],
|
||
index: declarationMatch.index,
|
||
end: initializerEnd
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findRawSqlAssignedPlainExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue;
|
||
|
||
const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("="));
|
||
if (equalsIndex === -1) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const statementEnd = findStatementEnd(structure, initializerStart);
|
||
const branchRawMatch = staticInitializerBranchMatch(
|
||
content,
|
||
structure,
|
||
initializerStart,
|
||
statementEnd,
|
||
completeRawSqlPlainExecutorAliasExpressionMatch
|
||
);
|
||
if (branchRawMatch?.branch) {
|
||
// 赋值式 plain raw alias 覆盖 `let runRaw; runRaw = maybe || db.$executeRawUnsafe`。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd });
|
||
continue;
|
||
}
|
||
|
||
for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) {
|
||
const rawMatchStart = rawMatch.index;
|
||
if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue;
|
||
|
||
const rawMatchEnd = rawMatch.end;
|
||
const initializerEnd = rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd);
|
||
if (initializerEnd === -1) continue;
|
||
|
||
// 赋值 alias 覆盖 `let execRaw; execRaw = db.$executeRawUnsafe` 这类后续绑定。
|
||
aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: initializerEnd });
|
||
break;
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function destructuringParts(structure, startIndex, endIndex) {
|
||
const parts = [];
|
||
let partStart = startIndex;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
let parenDepth = 0;
|
||
|
||
for (let cursor = startIndex; cursor < endIndex; cursor += 1) {
|
||
const char = structure[cursor];
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char !== "," || braceDepth !== 0 || bracketDepth !== 0 || parenDepth !== 0) continue;
|
||
|
||
parts.push({ start: partStart, end: cursor });
|
||
partStart = cursor + 1;
|
||
}
|
||
|
||
parts.push({ start: partStart, end: endIndex });
|
||
return parts;
|
||
}
|
||
|
||
function destructuredAliasNamesForStaticKeys(content, structure, bindingStart, bindingEnd, keys, beforeIndex) {
|
||
const names = [];
|
||
const allowedKeys = new Set(keys);
|
||
|
||
for (const part of destructuringParts(structure, bindingStart, bindingEnd)) {
|
||
const key = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex);
|
||
if (!allowedKeys.has(key)) continue;
|
||
|
||
const name = destructuringAliasName(structure, part.start, part.end);
|
||
if (name) names.push(name);
|
||
}
|
||
|
||
return names;
|
||
}
|
||
|
||
function staticDestructuringPropertyKeyValue(content, structure, partStart, partEnd, beforeIndex) {
|
||
const keyStart = skipWhitespaceForward(structure, partStart);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd);
|
||
if (keyEnd === -1) return null;
|
||
|
||
const expressionStart = structure[keyStart] === "[" ? keyStart + 1 : keyStart;
|
||
const expressionEnd = structure[keyStart] === "[" ? keyEnd - 1 : keyEnd;
|
||
const keySource = content.slice(expressionStart, expressionEnd);
|
||
if (structure[keyStart] !== "[" && /^[A-Za-z_$][\w$]*$/.test(keySource.trim())) return keySource.trim();
|
||
|
||
return staticJavaScriptExpressionValues(content, structure, keySource, expressionStart, beforeIndex, new Set(), 0)[0] ?? null;
|
||
}
|
||
|
||
function destructuringPropertyKeyEnd(structure, keyStart, partEnd) {
|
||
if (keyStart >= partEnd) return -1;
|
||
|
||
if (structure[keyStart] === "[") {
|
||
const closeBracketIndex = findMatchingBracket(structure, keyStart);
|
||
if (closeBracketIndex === -1 || closeBracketIndex >= partEnd) return -1;
|
||
const afterKey = skipWhitespaceForward(structure, closeBracketIndex + 1);
|
||
return structure[afterKey] === ":" ? closeBracketIndex + 1 : -1;
|
||
}
|
||
|
||
const literalEnd = findStringLiteralEnd(structure, keyStart);
|
||
if ((structure[keyStart] === '"' || structure[keyStart] === "'" || structure[keyStart] === "`") && literalEnd !== -1 && literalEnd < partEnd) {
|
||
const afterKey = skipWhitespaceForward(structure, literalEnd + 1);
|
||
return structure[afterKey] === ":" ? literalEnd + 1 : -1;
|
||
}
|
||
|
||
const identifierMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(keyStart, partEnd));
|
||
if (!identifierMatch) return -1;
|
||
|
||
const keyEnd = keyStart + identifierMatch[0].length;
|
||
const afterKey = skipWhitespaceForward(structure, keyEnd);
|
||
if (structure[afterKey] === ":" || afterKey === partEnd) return keyEnd;
|
||
return -1;
|
||
}
|
||
|
||
function destructuringAliasName(structure, partStart, partEnd) {
|
||
const keyStart = skipWhitespaceForward(structure, partStart);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd);
|
||
if (keyEnd === -1) return null;
|
||
|
||
const afterKey = skipWhitespaceForward(structure, keyEnd);
|
||
if (structure[afterKey] !== ":") {
|
||
const shorthandName = structure.slice(keyStart, keyEnd).trim();
|
||
return /^[A-Za-z_$][\w$]*$/.test(shorthandName) ? shorthandName : null;
|
||
}
|
||
|
||
const aliasStart = skipWhitespaceForward(structure, afterKey + 1);
|
||
const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(aliasStart, partEnd));
|
||
if (!aliasMatch) return null;
|
||
|
||
const aliasEnd = aliasStart + aliasMatch[0].length;
|
||
const afterAlias = skipWhitespaceForward(structure, aliasEnd);
|
||
return afterAlias === partEnd ? aliasMatch[0] : null;
|
||
}
|
||
|
||
function findRawSqlDestructuredExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const declarationPattern = /\b(?:const|let|var)\s*\{/g;
|
||
|
||
for (const declarationMatch of structure.matchAll(declarationPattern)) {
|
||
if (typeof declarationMatch.index !== "number") continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", declarationMatch.index);
|
||
if (openBraceIndex === -1) continue;
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(
|
||
content,
|
||
structure,
|
||
openBraceIndex + 1,
|
||
closeBraceIndex,
|
||
rawSqlExecutorNames,
|
||
declarationMatch.index
|
||
);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] === ":") {
|
||
const equalsAfterType = structure.indexOf("=", cursor + 1);
|
||
const semicolonAfterType = structure.indexOf(";", cursor + 1);
|
||
if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue;
|
||
cursor = skipWhitespaceForward(structure, equalsAfterType + 1);
|
||
} else {
|
||
if (structure[cursor] !== "=") continue;
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
}
|
||
|
||
const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor);
|
||
if (!receiverMatch) continue;
|
||
|
||
const expressionEnd = cursor + receiverMatch[0].length;
|
||
const statementEnd = findStatementEnd(structure, cursor);
|
||
const aliasEnd = simpleRawSqlAliasInitializerEnd(structure, expressionEnd, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 解构 alias 只覆盖简单 receiver,避免把复杂运行时对象误判为 Prisma raw executor。
|
||
for (const name of names) {
|
||
aliases.push({ name, index: declarationMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function findRawSqlAssignedDestructuredExecutorAliases(content, structure) {
|
||
const aliases = [];
|
||
const assignmentPattern = /\(\s*\{/g;
|
||
|
||
for (const assignmentMatch of structure.matchAll(assignmentPattern)) {
|
||
if (typeof assignmentMatch.index !== "number") continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", assignmentMatch.index);
|
||
if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue;
|
||
|
||
const openBraceIndex = structure.indexOf("{", openParenIndex);
|
||
if (openBraceIndex === -1) continue;
|
||
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
const closeParenIndex = findMatchingParen(structure, openParenIndex);
|
||
if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue;
|
||
|
||
const names = destructuredAliasNamesForStaticKeys(
|
||
content,
|
||
structure,
|
||
openBraceIndex + 1,
|
||
closeBraceIndex,
|
||
rawSqlExecutorNames,
|
||
assignmentMatch.index
|
||
);
|
||
if (names.length === 0) continue;
|
||
|
||
let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1);
|
||
if (structure[cursor] !== "=") continue;
|
||
|
||
cursor = skipWhitespaceForward(structure, cursor + 1);
|
||
const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor);
|
||
if (!receiverMatch) continue;
|
||
|
||
const expressionEnd = cursor + receiverMatch[0].length;
|
||
if (skipWhitespaceForward(structure, expressionEnd) !== closeParenIndex) continue;
|
||
|
||
const statementEnd = findStatementEnd(structure, openParenIndex);
|
||
const aliasEnd = simpleRawSqlAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd);
|
||
if (aliasEnd === -1) continue;
|
||
|
||
// 声明后的解构赋值 alias 覆盖 `let execRaw; ({ $executeRawUnsafe: execRaw } = db)`。
|
||
for (const name of names) {
|
||
aliases.push({ name, index: assignmentMatch.index, end: aliasEnd });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function isMemberIdentifierReference(structure, identifierIndex) {
|
||
const previousIndex = skipWhitespaceBackward(structure, identifierIndex - 1);
|
||
return previousIndex >= 0 && structure[previousIndex] === ".";
|
||
}
|
||
|
||
function firstArrayElementSource(content, structure, sourceStart, sourceEnd) {
|
||
return nthArrayElementSource(content, structure, { index: sourceStart, end: sourceEnd }, 0);
|
||
}
|
||
|
||
function nthArrayElementSource(content, structure, arrayArgument, elementIndex) {
|
||
if (!arrayArgument) return null;
|
||
|
||
const sourceStart = arrayArgument.index;
|
||
const sourceEnd = arrayArgument.end;
|
||
const arrayStart = skipWhitespaceForward(structure, sourceStart);
|
||
const arrayEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
if (arrayStart >= arrayEnd || structure[arrayStart] !== "[") return null;
|
||
|
||
const closeBracketIndex = findMatchingBracket(structure, arrayStart);
|
||
if (closeBracketIndex === -1 || closeBracketIndex !== arrayEnd - 1) return null;
|
||
|
||
return nthCallArgumentSource(content, structure, arrayStart, closeBracketIndex, elementIndex);
|
||
}
|
||
|
||
function rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, beforeIndex) {
|
||
const sqlArgument = firstArrayElementSource(content, structure, arrayArgument.index, arrayArgument.end);
|
||
return sqlArgument ? [{ ...sqlArgument, beforeIndex }] : [];
|
||
}
|
||
|
||
function rawSqlExecutorDirectAliases(content, structure) {
|
||
return findRawSqlBoundExecutorAliases(content, structure)
|
||
.concat(findRawSqlAssignedBoundExecutorAliases(content, structure))
|
||
.concat(findRawSqlPlainExecutorAliases(content, structure))
|
||
.concat(findRawSqlAssignedPlainExecutorAliases(content, structure))
|
||
.concat(findRawSqlDestructuredExecutorAliases(content, structure))
|
||
.concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure));
|
||
}
|
||
|
||
function rawSqlExecutorAliases(content, structure) {
|
||
const directAliases = propagateStaticIdentifierAliases(structure, rawSqlExecutorDirectAliases(content, structure), (_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
const wrappers = rawSqlExecutorWrapperSources(content, structure, directAliases);
|
||
const aliases = findRawSqlBoundExecutorAliases(content, structure)
|
||
.concat(findRawSqlAssignedBoundExecutorAliases(content, structure))
|
||
.concat(findRawSqlPlainExecutorAliases(content, structure))
|
||
.concat(findRawSqlAssignedPlainExecutorAliases(content, structure))
|
||
.concat(findRawSqlDestructuredExecutorAliases(content, structure))
|
||
.concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure))
|
||
.concat(findStaticWrapperExtractionAliases(content, structure, wrappers));
|
||
|
||
return propagateStaticIdentifierAliases(structure, aliases, (_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
}
|
||
|
||
function isRawSqlExecutorAliasTarget(structure, targetArgument, aliases) {
|
||
const aliasName = simpleIdentifierSourceName(structure, targetArgument.index, targetArgument.end);
|
||
if (!aliasName) return false;
|
||
|
||
// Reflect.apply 只能使用调用点之前已建立的同文件 raw executor alias。
|
||
return Boolean(latestStaticAliasRecordAt(structure, aliases, aliasName, targetArgument.index));
|
||
}
|
||
|
||
function isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases) {
|
||
return (
|
||
targetArgument &&
|
||
(hasRawSqlExecutorAccessInRange(content, structure, targetArgument.index, targetArgument.end) ||
|
||
isRawSqlExecutorAliasTarget(structure, targetArgument, aliases))
|
||
);
|
||
}
|
||
|
||
function rawSqlInvocationSourcesFromFunctionPrototypeArrayArgument(content, structure, arrayArgument, methodKind, beforeIndex) {
|
||
if (methodKind === "call") {
|
||
const sqlArgument = nthArrayElementSource(content, structure, arrayArgument, 1);
|
||
return sqlArgument ? [{ ...sqlArgument, beforeIndex }] : [];
|
||
}
|
||
|
||
if (methodKind === "apply") {
|
||
const nestedArrayArgument = nthArrayElementSource(content, structure, arrayArgument, 1);
|
||
return nestedArrayArgument ? rawSqlInvocationSourcesFromArrayArgument(content, structure, nestedArrayArgument, beforeIndex) : [];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
function rawSqlFunctionPrototypeInvocationSources(content, structure, builtinInvokerAliases) {
|
||
const sources = [];
|
||
const aliases = rawSqlExecutorAliases(content, structure);
|
||
const boundTargetAliases = findFunctionPrototypeBoundTargetAliases(content, structure);
|
||
|
||
for (const invocation of functionPrototypeCallInvocationMatches(content, structure, builtinInvokerAliases)) {
|
||
const targetArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 0);
|
||
if (!isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases)) continue;
|
||
|
||
if (invocation.methodKind === "call") {
|
||
// Function.prototype.call.call(raw, thisArg, sql, ...) 的 SQL 是外层调用第三参。
|
||
const sqlArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 2);
|
||
if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: invocation.index });
|
||
continue;
|
||
}
|
||
|
||
// Function.prototype.apply.call(raw, thisArg, [sql, ...]) 的 SQL 在第三参数数组首项。
|
||
const arrayArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 2);
|
||
if (arrayArgument) sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index));
|
||
}
|
||
|
||
for (const invocation of functionPrototypeBoundTargetInvocationMatches(content, structure, boundTargetAliases)) {
|
||
if (!isRawSqlExecutorTargetArgument(content, structure, invocation.targetArgument, aliases)) continue;
|
||
|
||
if (invocation.methodKind === "call") {
|
||
// Function.prototype.call.bind(raw)(thisArg, sql, ...) 的 SQL 是绑定后调用的第二参。
|
||
const sqlArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1);
|
||
if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: invocation.index });
|
||
continue;
|
||
}
|
||
|
||
// Function.prototype.apply.bind(raw)(thisArg, [sql, ...]) 的 SQL 在绑定后调用第二参数数组首项。
|
||
const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1);
|
||
if (arrayArgument) sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index));
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function rawSqlReflectApplyInvocationSources(content, structure, builtinInvokerAliases) {
|
||
const sources = [];
|
||
const aliases = rawSqlExecutorAliases(content, structure);
|
||
|
||
for (const invocation of reflectApplyInvocationMatches(content, structure, builtinInvokerAliases)) {
|
||
const targetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 0);
|
||
if (!targetArgument) continue;
|
||
|
||
const functionPrototypeMethod = functionPrototypeMethodKindFromArgument(content, structure, targetArgument, builtinInvokerAliases);
|
||
if (functionPrototypeMethod) {
|
||
const invokedFunctionArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 1);
|
||
if (!isRawSqlExecutorTargetArgument(content, structure, invokedFunctionArgument, aliases)) continue;
|
||
|
||
const arrayArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 2);
|
||
if (!arrayArgument) continue;
|
||
|
||
sources.push(...rawSqlInvocationSourcesFromFunctionPrototypeArrayArgument(content, structure, arrayArgument, functionPrototypeMethod, invocation.index));
|
||
continue;
|
||
}
|
||
|
||
if (!isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases)) continue;
|
||
|
||
const arrayArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 2);
|
||
if (!arrayArgument) continue;
|
||
|
||
sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index));
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function rawSqlInvocationSourcesFromOperation(content, structure, operation, beforeIndex) {
|
||
const sources = [];
|
||
if (!operation) return sources;
|
||
|
||
if (operation.kind === "direct") {
|
||
const closeParenIndex = findMatchingParen(structure, operation.callStart);
|
||
if (closeParenIndex === -1) return sources;
|
||
const sqlArgument = nthCallArgumentSource(content, structure, operation.callStart, closeParenIndex, 0);
|
||
if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex });
|
||
return sources;
|
||
}
|
||
|
||
if (operation.kind === "template") {
|
||
const templateEnd = findStringLiteralEnd(content, operation.templateStart);
|
||
if (templateEnd !== -1) {
|
||
sources.push({
|
||
source: content.slice(operation.templateStart, templateEnd + 1),
|
||
index: operation.templateStart,
|
||
end: templateEnd + 1,
|
||
beforeIndex
|
||
});
|
||
}
|
||
return sources;
|
||
}
|
||
|
||
if (operation.kind !== "call" && operation.kind !== "bind" && operation.kind !== "apply") return sources;
|
||
|
||
const methodOpenParenIndex = operation.openParenIndex ?? structure.indexOf("(", operation.methodNameEnd);
|
||
if (methodOpenParenIndex === -1) return sources;
|
||
const methodCloseParenIndex = operation.closeParenIndex ?? findMatchingParen(structure, methodOpenParenIndex);
|
||
if (methodCloseParenIndex === -1) return sources;
|
||
|
||
if (operation.kind === "call") {
|
||
// .call(thisArg, sql, ...) 的 SQL 是第二个参数。
|
||
const sqlArgument = nthCallArgumentSource(content, structure, methodOpenParenIndex, methodCloseParenIndex, 1);
|
||
if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex });
|
||
return sources;
|
||
}
|
||
|
||
if (operation.kind === "apply") {
|
||
// .apply(thisArg, [sql, ...]) 的 SQL 在参数数组首项,和 Reflect.apply 共享同一提取语义。
|
||
const arrayArgument = nthCallArgumentSource(content, structure, methodOpenParenIndex, methodCloseParenIndex, 1);
|
||
return arrayArgument ? rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, beforeIndex) : sources;
|
||
}
|
||
|
||
let boundCallStart = skipWhitespaceForward(structure, methodCloseParenIndex + 1);
|
||
if (structure.startsWith("?.", boundCallStart)) {
|
||
boundCallStart = skipWhitespaceForward(structure, boundCallStart + 2);
|
||
}
|
||
|
||
if (structure[boundCallStart] !== "(") return sources;
|
||
const boundCloseParenIndex = findMatchingParen(structure, boundCallStart);
|
||
if (boundCloseParenIndex === -1) return sources;
|
||
const sqlArgument = nthCallArgumentSource(content, structure, boundCallStart, boundCloseParenIndex, 0);
|
||
if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex });
|
||
return sources;
|
||
}
|
||
|
||
function rawSqlSourceMatcher(aliases = []) {
|
||
return (content, structure, sourceStart, sourceEnd) => {
|
||
const accessMatches = rawSqlExecutorAccessMatches(content, structure, sourceStart, sourceEnd);
|
||
|
||
for (const rawMatch of accessMatches) {
|
||
if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, sourceStart, rawMatch.index)) continue;
|
||
|
||
const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatch.end, sourceEnd);
|
||
if (bindCloseParenIndex !== -1 && isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, sourceEnd)) {
|
||
return { kind: "rawExecutor", index: rawMatch.index, end: bindCloseParenIndex + 1 };
|
||
}
|
||
|
||
if (isAtStaticWrapperExpressionEnd(structure, rawMatch.end, sourceEnd)) {
|
||
return { kind: "rawExecutor", index: rawMatch.index, end: rawMatch.end };
|
||
}
|
||
}
|
||
|
||
return staticIdentifierAliasSourceMatchAt(structure, aliases, sourceStart, sourceEnd, sourceStart);
|
||
};
|
||
}
|
||
|
||
function rawSqlExecutorWrapperSources(content, structure, aliases = []) {
|
||
return staticWrapperSources(content, structure, rawSqlSourceMatcher(aliases));
|
||
}
|
||
|
||
function rawSqlWrappedExecutorInvocationSources(content, structure) {
|
||
const sources = [];
|
||
const directAliases = propagateStaticIdentifierAliases(structure, rawSqlExecutorDirectAliases(content, structure), (_sourceAlias, initializer) => ({
|
||
name: initializer.name,
|
||
index: initializer.index,
|
||
end: initializer.end
|
||
}));
|
||
|
||
for (const usage of staticWrapperSourceUsageMatches(content, structure, rawSqlExecutorWrapperSources(content, structure, directAliases))) {
|
||
const operation = rawSqlMemberOperation(content, structure, usage.end);
|
||
sources.push(...rawSqlInvocationSourcesFromOperation(content, structure, operation, usage.index));
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function rawSqlAliasInvocationSources(content, structure, aliases) {
|
||
const sources = [];
|
||
|
||
for (const alias of aliases) {
|
||
const aliasCallPattern = new RegExp(String.raw`(?:^|[^\w$])(${escapeRegExp(alias.name)})\b\s*(?:<[^>()` + "`" + String.raw`]*>)?\s*`, "g");
|
||
|
||
for (const aliasMatch of structure.matchAll(aliasCallPattern)) {
|
||
if (typeof aliasMatch.index !== "number" || aliasMatch.index <= alias.end) continue;
|
||
const aliasIndex = aliasMatch.index + aliasMatch[0].indexOf(alias.name);
|
||
if (aliasIndex <= alias.end) continue;
|
||
if (isMemberIdentifierReference(structure, aliasIndex)) continue;
|
||
|
||
const operationStart = skipWhitespaceForward(structure, aliasIndex + alias.name.length);
|
||
const operation = rawSqlMemberOperation(content, structure, operationStart);
|
||
sources.push(...rawSqlInvocationSourcesFromOperation(content, structure, operation, aliasIndex));
|
||
}
|
||
}
|
||
|
||
return sources;
|
||
}
|
||
|
||
function rawSqlBoundAliasInvocationSources(content, structure) {
|
||
return rawSqlAliasInvocationSources(
|
||
content,
|
||
structure,
|
||
findRawSqlBoundExecutorAliases(content, structure).concat(findRawSqlAssignedBoundExecutorAliases(content, structure))
|
||
);
|
||
}
|
||
|
||
function rawSqlPlainAliasInvocationSources(content, structure) {
|
||
return rawSqlAliasInvocationSources(
|
||
content,
|
||
structure,
|
||
findRawSqlPlainExecutorAliases(content, structure)
|
||
.concat(findRawSqlAssignedPlainExecutorAliases(content, structure))
|
||
.concat(findRawSqlDestructuredExecutorAliases(content, structure))
|
||
.concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure))
|
||
);
|
||
}
|
||
|
||
function rawSqlInvocationSources(content, structure = rawSqlStructure(content)) {
|
||
const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure);
|
||
const sources = [];
|
||
const builtinInvokerAliases = findStaticBuiltinInvokerAliases(content, scanStructure);
|
||
|
||
for (const match of rawSqlExecutorAccessMatches(content, scanStructure)) {
|
||
let callStart = skipWhitespaceForward(scanStructure, match.end);
|
||
const operation = rawSqlMemberOperation(content, scanStructure, callStart);
|
||
sources.push(...rawSqlInvocationSourcesFromOperation(content, scanStructure, operation, match.index));
|
||
}
|
||
|
||
return sources.concat(
|
||
rawSqlReflectApplyInvocationSources(content, scanStructure, builtinInvokerAliases),
|
||
rawSqlFunctionPrototypeInvocationSources(content, scanStructure, builtinInvokerAliases),
|
||
rawSqlWrappedExecutorInvocationSources(content, scanStructure),
|
||
rawSqlAliasInvocationSources(content, scanStructure, rawSqlExecutorAliases(content, scanStructure))
|
||
);
|
||
}
|
||
|
||
function simpleIdentifierSourceName(structure, sourceStart, sourceEnd) {
|
||
const identifierStart = skipWhitespaceForward(structure, sourceStart);
|
||
const identifierEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
if (identifierStart >= identifierEnd) return null;
|
||
|
||
const source = structure.slice(identifierStart, identifierEnd);
|
||
return /^[A-Za-z_$][\w$]*$/.test(source) ? source : null;
|
||
}
|
||
|
||
function hasRawSqlInvocationMatching(content, predicate, structure = rawSqlStructure(content)) {
|
||
for (const invocation of rawSqlInvocationSources(content, structure)) {
|
||
const { source, index, end, beforeIndex } = invocation;
|
||
if (rawSqlScanCandidateSources(content, structure, source, index, end, beforeIndex).some((candidateSource) => predicate(candidateSource))) {
|
||
return true;
|
||
}
|
||
|
||
const variableName = simpleIdentifierSourceName(structure, index, end);
|
||
if (
|
||
variableName &&
|
||
findVariableInitializerSources(content, structure, variableName, beforeIndex).some(({ source: variableSource, index: variableIndex, end: variableEnd }) =>
|
||
rawSqlScanCandidateSources(content, structure, variableSource, variableIndex, variableEnd, beforeIndex).some((candidateSource) =>
|
||
predicate(candidateSource)
|
||
)
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function hasDynamicIdentifierRawSqlWrite(content) {
|
||
return hasRawSqlInvocationMatching(content, hasDynamicRawSqlWrite);
|
||
}
|
||
|
||
function hasRawJobStatusWrite(content) {
|
||
// raw SQL 检测仍从原始 content 切片读取 SQL 字符串;structure 只负责可靠找到调用边界。
|
||
return hasRawSqlInvocationMatching(content, hasRawJobRuntimeFieldWrite);
|
||
}
|
||
|
||
function hasForbiddenRawJobStatusWrite(filePath, content) {
|
||
// migration.sql 没有 Prisma raw 调用包装,必须直接扫描 SQL 文件本体。
|
||
if (filePath.endsWith(".sql")) return hasRawJobRuntimeFieldWrite(content);
|
||
return hasRawJobStatusWrite(content);
|
||
}
|
||
|
||
function hasRawAuditLogMutation(sqlSource, options = {}) {
|
||
const sql = normalizeRawSqlScanSource(sqlSource);
|
||
const auditLogTable = sqlTableReferencePattern("AuditLog");
|
||
const auditLogAppendOnlyFunction = sqlSpecificFunctionReferencePattern(auditLogAppendOnlyFunctionName);
|
||
const auditLogAppendOnlyFunctionList = sqlFunctionListContainingPattern(auditLogAppendOnlyFunction);
|
||
const identifier = sqlBareIdentifierPattern();
|
||
const triggerIdentifier = sqlTriggerIdentifierPattern();
|
||
// PostgreSQL UPDATE 允许目标表后带 `*` 继承标记;AuditLog append-only 扫描必须覆盖该合法语法。
|
||
const createsPlainAuditLogAppendOnlyFunction = new RegExp(
|
||
String.raw`\bCREATE\s+FUNCTION\s+${auditLogAppendOnlyFunction}\b`,
|
||
"i"
|
||
).test(sql);
|
||
const createsOrReplacesAuditLogAppendOnlyFunction = new RegExp(
|
||
String.raw`\bCREATE\s+OR\s+REPLACE\s+FUNCTION\s+${auditLogAppendOnlyFunction}\b`,
|
||
"i"
|
||
).test(sql);
|
||
return (
|
||
new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${auditLogTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test(sql) ||
|
||
new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${auditLogTable}\b`, "i").test(sql) ||
|
||
new RegExp(
|
||
String.raw`\bINSERT\s+INTO\s+(?:ONLY\s+)?${auditLogTable}\b[\s\S]*?\bON\s+CONFLICT\b[\s\S]*?\bDO\s+UPDATE\b`,
|
||
"i"
|
||
).test(sql) ||
|
||
new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${auditLogTable}(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test(sql) ||
|
||
new RegExp(sqlTruncateTableListPattern(auditLogTable), "i").test(sql) ||
|
||
new RegExp(sqlDropTableListPattern(auditLogTable), "i").test(sql) ||
|
||
new RegExp(String.raw`\bDROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?${triggerIdentifier}\s+ON\s+(?:ONLY\s+)?${auditLogTable}\b`, "i").test(sql) ||
|
||
new RegExp(
|
||
String.raw`\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?${auditLogTable}\s+(?:DISABLE\s+(?:TRIGGER|RULE)\b|DROP\b|RENAME\b)`,
|
||
"i"
|
||
).test(sql) ||
|
||
new RegExp(String.raw`\bDROP\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${auditLogAppendOnlyFunctionList}(?=\s|;|$)`, "i").test(
|
||
sql
|
||
) ||
|
||
(!options.allowAuditLogAppendOnlyFunctionPlainCreate && createsPlainAuditLogAppendOnlyFunction) ||
|
||
(!options.allowAuditLogAppendOnlyFunctionCreateOrReplace && createsOrReplacesAuditLogAppendOnlyFunction) ||
|
||
new RegExp(String.raw`\bALTER\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${auditLogAppendOnlyFunction}(?=\s|;|$)`, "i").test(sql)
|
||
);
|
||
}
|
||
|
||
function hasForbiddenRawAuditLogMutation(filePath, content, structure) {
|
||
const auditSqlOptions = auditLogAppendOnlyFunctionCreateOptions(filePath, content);
|
||
|
||
if (filePath.endsWith(".sql")) return hasRawAuditLogMutation(content, auditSqlOptions);
|
||
|
||
for (const invocation of rawSqlInvocationSources(content, structure)) {
|
||
const { source, index, end, beforeIndex } = invocation;
|
||
const hasForbiddenSql =
|
||
rawSqlScanCandidateSources(content, structure, source, index, end, beforeIndex).some((candidateSource) =>
|
||
hasRawAuditLogMutation(candidateSource, auditSqlOptions)
|
||
) ||
|
||
(() => {
|
||
const variableName = simpleIdentifierSourceName(structure, index, end);
|
||
return (
|
||
variableName !== null &&
|
||
findVariableInitializerSources(content, structure, variableName, beforeIndex).some(
|
||
({ source: variableSource, index: variableIndex, end: variableEnd }) =>
|
||
rawSqlScanCandidateSources(content, structure, variableSource, variableIndex, variableEnd, beforeIndex).some((candidateSource) =>
|
||
hasRawAuditLogMutation(candidateSource, auditSqlOptions)
|
||
)
|
||
)
|
||
);
|
||
})();
|
||
|
||
if (!hasForbiddenSql) continue;
|
||
if (isAllowedAuditTriggerAssertion(filePath, content, structure, beforeIndex)) continue;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function jobExecutionStoreClassSpans(content) {
|
||
const spans = [];
|
||
const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(content));
|
||
const classPattern = /\b(?:export\s+)?class\s+JobExecutionStore\b/g;
|
||
|
||
for (const match of structure.matchAll(classPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const openBraceIndex = structure.indexOf("{", match.index + match[0].length);
|
||
if (openBraceIndex === -1) continue;
|
||
const closeBraceIndex = findMatchingBrace(structure, openBraceIndex);
|
||
if (closeBraceIndex === -1) continue;
|
||
spans.push({ start: match.index, end: closeBraceIndex + 1 });
|
||
}
|
||
|
||
return spans;
|
||
}
|
||
|
||
function maskSpans(content, spans) {
|
||
if (spans.length === 0) return content;
|
||
let output = "";
|
||
let cursor = 0;
|
||
|
||
for (const span of spans.toSorted((left, right) => left.start - right.start)) {
|
||
if (span.start < cursor) continue;
|
||
output += content.slice(cursor, span.start);
|
||
output += " ".repeat(span.end - span.start);
|
||
cursor = span.end;
|
||
}
|
||
|
||
return output + content.slice(cursor);
|
||
}
|
||
|
||
function maskJobExecutionStoreClassBodies(content) {
|
||
// 只遮蔽 JobExecutionStore 自身;同文件里的 API service / controller 仍要按普通边界扫描。
|
||
return maskSpans(content, jobExecutionStoreClassSpans(content));
|
||
}
|
||
|
||
function hasForbiddenMigrationGuardSetter(content) {
|
||
const sql = stripSqlComments(content);
|
||
return /set_config\s*\(|SET\s+LOCAL/i.test(sql);
|
||
}
|
||
|
||
function hasStateTransitionSpecifier(value) {
|
||
return /state-transition(?:\/index\.js)?/.test(value);
|
||
}
|
||
|
||
function unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd) {
|
||
let trimmedStart = skipWhitespaceForward(structure, sourceStart);
|
||
let trimmedEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1;
|
||
|
||
while (trimmedStart < trimmedEnd && structure[trimmedStart] === "(") {
|
||
const closeParenIndex = findMatchingParen(structure, trimmedStart);
|
||
if (closeParenIndex !== trimmedEnd - 1) break;
|
||
|
||
const innerStart = skipWhitespaceForward(structure, trimmedStart + 1);
|
||
const innerEnd = skipWhitespaceBackward(structure, closeParenIndex - 1) + 1;
|
||
if (innerStart >= innerEnd) break;
|
||
|
||
// 只剥离完整包住表达式的括号,避免改变 ("a") + b 这类拼接分片边界。
|
||
trimmedStart = innerStart;
|
||
trimmedEnd = innerEnd;
|
||
}
|
||
|
||
return { start: trimmedStart, end: trimmedEnd };
|
||
}
|
||
|
||
function decodeStaticTemplateLiteralExpressionValue(content, structure, sourceStart, sourceEnd, beforeIndex, seenVariables, depth) {
|
||
if (content[sourceStart] !== "`") return null;
|
||
|
||
const templateEnd = findStringLiteralEnd(content, sourceStart);
|
||
if (templateEnd !== sourceEnd - 1) return null;
|
||
|
||
let output = "";
|
||
let cursor = sourceStart + 1;
|
||
|
||
while (cursor < templateEnd) {
|
||
if (content[cursor] === "\\") {
|
||
if (cursor + 1 < templateEnd) {
|
||
output += decodeJavaScriptStringLiteralBody(content.slice(cursor, cursor + 2));
|
||
cursor += 2;
|
||
continue;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
if (content[cursor] === "$" && content[cursor + 1] === "{") {
|
||
const expressionStart = cursor + 2;
|
||
const expressionEnd = findMatchingBrace(content, cursor + 1);
|
||
if (expressionEnd === -1 || expressionEnd > templateEnd) return null;
|
||
const expressionStructure =
|
||
structure.slice(0, expressionStart) + content.slice(expressionStart, expressionEnd) + structure.slice(expressionEnd);
|
||
|
||
const values = staticJavaScriptExpressionValues(
|
||
content,
|
||
expressionStructure,
|
||
content.slice(expressionStart, expressionEnd),
|
||
expressionStart,
|
||
beforeIndex,
|
||
seenVariables,
|
||
depth + 1
|
||
);
|
||
if (values.length !== 1) return null;
|
||
|
||
output += values[0];
|
||
cursor = expressionEnd + 1;
|
||
continue;
|
||
}
|
||
|
||
output += content[cursor];
|
||
cursor += 1;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
function staticJavaScriptExpressionValues(content, structure, source, sourceStart, beforeIndex, seenVariables, depth) {
|
||
if (depth >= rawSqlVariableResolutionMaxDepth) return [];
|
||
|
||
const { start: trimmedStart, end: trimmedEnd } = unwrapCompleteParenthesizedJavaScriptExpression(
|
||
structure,
|
||
sourceStart,
|
||
sourceStart + source.length
|
||
);
|
||
if (trimmedStart >= trimmedEnd) return [];
|
||
|
||
const expression = content.slice(trimmedStart, trimmedEnd);
|
||
const literalValue = decodeStaticJavaScriptStringLiteralSource(expression);
|
||
if (literalValue !== null) return [literalValue];
|
||
|
||
const templateValue = decodeStaticTemplateLiteralExpressionValue(content, structure, trimmedStart, trimmedEnd, beforeIndex, seenVariables, depth);
|
||
if (templateValue !== null) return [templateValue];
|
||
|
||
const identifierName = simpleIdentifierSourceName(structure, trimmedStart, trimmedEnd);
|
||
if (identifierName) {
|
||
return staticStringVariableInitializers(content, structure, identifierName, beforeIndex, seenVariables, depth + 1);
|
||
}
|
||
|
||
const expressionStructure = structure.slice(trimmedStart, trimmedEnd);
|
||
let parts = [];
|
||
let partStart = 0;
|
||
let parenDepth = 0;
|
||
let braceDepth = 0;
|
||
let bracketDepth = 0;
|
||
|
||
for (let cursor = 0; cursor < expressionStructure.length; cursor += 1) {
|
||
const char = expressionStructure[cursor];
|
||
if (char === "(") parenDepth += 1;
|
||
if (char === ")") parenDepth -= 1;
|
||
if (char === "{") braceDepth += 1;
|
||
if (char === "}") braceDepth -= 1;
|
||
if (char === "[") bracketDepth += 1;
|
||
if (char === "]") bracketDepth -= 1;
|
||
if (char !== "+" || parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue;
|
||
|
||
parts.push({ start: trimmedStart + partStart, end: trimmedStart + cursor });
|
||
partStart = cursor + 1;
|
||
}
|
||
|
||
if (parts.length === 0) return [];
|
||
parts.push({ start: trimmedStart + partStart, end: trimmedEnd });
|
||
|
||
const values = [];
|
||
for (const part of parts) {
|
||
const partSource = content.slice(part.start, part.end);
|
||
const partValues = staticJavaScriptExpressionValues(content, structure, partSource, part.start, beforeIndex, seenVariables, depth + 1);
|
||
if (partValues.length !== 1) return [];
|
||
values.push(partValues[0]);
|
||
}
|
||
|
||
// 只折叠全静态字符串拼接;任何动态片段都保持未知,不影响其它动态 import。
|
||
return [values.join("")];
|
||
}
|
||
|
||
function staticStringVariableInitializers(content, structure, variableName, beforeIndex, seenVariables = new Set(), depth = 0) {
|
||
if (depth >= rawSqlVariableResolutionMaxDepth || seenVariables.has(variableName)) return [];
|
||
|
||
const values = [];
|
||
const nestedSeenVariables = new Set(seenVariables);
|
||
nestedSeenVariables.add(variableName);
|
||
const initializerPatterns = [
|
||
new RegExp(String.raw`\b(?:const|let|var)\s+${escapeRegExp(variableName)}\b(?:\s*:[^=;]+)?\s*=`, "g"),
|
||
new RegExp(String.raw`\b${escapeRegExp(variableName)}\b\s*=`, "g")
|
||
];
|
||
|
||
for (const initializerPattern of initializerPatterns) {
|
||
for (const match of structure.matchAll(initializerPattern)) {
|
||
if (typeof match.index !== "number" || match.index >= beforeIndex) continue;
|
||
|
||
const variableOffset = match[0].indexOf(variableName);
|
||
const variableIndex = match.index + variableOffset;
|
||
if (initializerPattern !== initializerPatterns[0] && isMemberIdentifierReference(structure, variableIndex)) continue;
|
||
if (initializerPattern !== initializerPatterns[0] && /\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, match.index - 16), variableIndex))) {
|
||
continue;
|
||
}
|
||
|
||
const equalsIndex = structure.indexOf("=", match.index + match[0].lastIndexOf("="));
|
||
if (equalsIndex === -1 || equalsIndex >= beforeIndex) continue;
|
||
if ("=><!".includes(structure[equalsIndex - 1] ?? "") || ["=", ">"].includes(structure[equalsIndex + 1] ?? "")) continue;
|
||
|
||
// 静态 key alias 既支持声明初始化,也支持调用点前的简单后续赋值。
|
||
const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1);
|
||
const initializerEnd = findStatementEnd(structure, initializerStart);
|
||
values.push(
|
||
...staticJavaScriptExpressionValues(
|
||
content,
|
||
structure,
|
||
content.slice(initializerStart, initializerEnd),
|
||
initializerStart,
|
||
beforeIndex,
|
||
nestedSeenVariables,
|
||
depth
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
return values;
|
||
}
|
||
|
||
function hasStateTransitionVariableImport(content) {
|
||
const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(content));
|
||
const dynamicImportPattern = /\b(?:import|require)\s*\(/g;
|
||
|
||
for (const match of structure.matchAll(dynamicImportPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const openParenIndex = structure.indexOf("(", match.index);
|
||
const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
const argumentStart = skipWhitespaceForward(structure, openParenIndex + 1);
|
||
const argumentEnd = skipWhitespaceBackward(structure, closeParenIndex - 1) + 1;
|
||
if (argumentStart >= argumentEnd) continue;
|
||
|
||
// 动态 import/require 的实参统一复用静态 JS 表达式折叠,覆盖 identifier、递归 alias 与直接字符串拼接。
|
||
if (
|
||
staticJavaScriptExpressionValues(
|
||
content,
|
||
structure,
|
||
content.slice(argumentStart, argumentEnd),
|
||
argumentStart,
|
||
match.index,
|
||
new Set(),
|
||
0
|
||
).some(hasStateTransitionSpecifier)
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return hasStateTransitionRequireAliasImport(content, structure);
|
||
}
|
||
|
||
function findRequireAliases(structure) {
|
||
const aliases = [];
|
||
const destructuringPattern = /\b(?:const|let|var)\s*\{\s*require\s*:\s*([A-Za-z_$][\w$]*)\s*\}\s*=\s*module\b/g;
|
||
const patterns = [
|
||
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=\s*require\b/g,
|
||
/\b([A-Za-z_$][\w$]*)\b\s*=\s*require\b/g
|
||
];
|
||
|
||
for (const match of structure.matchAll(destructuringPattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
|
||
const moduleIndex = match.index + match[0].lastIndexOf("module");
|
||
const afterModule = skipWhitespaceForward(structure, moduleIndex + "module".length);
|
||
const statementEnd = findStatementEnd(structure, moduleIndex);
|
||
if (afterModule !== statementEnd && !hasLineTerminator(structure.slice(afterModule, statementEnd))) continue;
|
||
|
||
// CommonJS module.require 的解构 alias 只接受简单同文件声明。
|
||
aliases.push({ name: match[1], index: match.index, end: afterModule });
|
||
}
|
||
|
||
for (const pattern of patterns) {
|
||
for (const match of structure.matchAll(pattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
if (isMemberIdentifierReference(structure, match.index + match[0].indexOf(match[1]))) continue;
|
||
|
||
const requireIndex = match.index + match[0].lastIndexOf("require");
|
||
const afterRequire = skipWhitespaceForward(structure, requireIndex + "require".length);
|
||
const statementEnd = findStatementEnd(structure, requireIndex);
|
||
if (afterRequire !== statementEnd && !hasLineTerminator(structure.slice(afterRequire, statementEnd))) continue;
|
||
|
||
// require alias 只跟踪简单声明/赋值,复杂初始化器不进入 import 边界判断。
|
||
aliases.push({ name: match[1], index: match.index, end: afterRequire });
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
}
|
||
|
||
function hasStateTransitionRequireAliasImport(content, structure) {
|
||
for (const alias of findRequireAliases(structure)) {
|
||
const aliasCallPattern = new RegExp(String.raw`\b${escapeRegExp(alias.name)}\b\s*\(`, "g");
|
||
|
||
for (const match of structure.matchAll(aliasCallPattern)) {
|
||
if (typeof match.index !== "number" || match.index <= alias.end) continue;
|
||
if (isMemberIdentifierReference(structure, match.index)) continue;
|
||
|
||
const openParenIndex = structure.indexOf("(", match.index + match[0].length - 1);
|
||
const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex);
|
||
if (closeParenIndex === -1) continue;
|
||
|
||
const argument = nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, 0);
|
||
if (!argument) continue;
|
||
|
||
if (
|
||
staticJavaScriptExpressionValues(content, structure, argument.source, argument.index, match.index, new Set(), 0).some(
|
||
hasStateTransitionSpecifier
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function javaScriptStringLiteralSourcePattern() {
|
||
return String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|` + "`(?:\\\\.|[^`\\\\])*`" + String.raw`)`;
|
||
}
|
||
|
||
function staticModuleSpecifierValues(content) {
|
||
const values = [];
|
||
const structure = maskCommentsOutsideStringLiterals(content);
|
||
const literalSource = javaScriptStringLiteralSourcePattern();
|
||
const patterns = [
|
||
new RegExp(String.raw`\bimport\s+(${literalSource})`, "g"),
|
||
new RegExp(String.raw`\bfrom\s+(${literalSource})`, "g")
|
||
];
|
||
|
||
for (const pattern of patterns) {
|
||
for (const match of structure.matchAll(pattern)) {
|
||
if (typeof match.index !== "number") continue;
|
||
const literalOffset = match[0].lastIndexOf(match[1]);
|
||
const literalStart = match.index + literalOffset;
|
||
const decodedValue = decodeStaticJavaScriptStringLiteralSource(content.slice(literalStart, literalStart + match[1].length));
|
||
if (decodedValue !== null) values.push(decodedValue);
|
||
}
|
||
}
|
||
|
||
return values;
|
||
}
|
||
|
||
function hasStaticStateTransitionSpecifier(content) {
|
||
return staticModuleSpecifierValues(content).some(hasStateTransitionSpecifier);
|
||
}
|
||
|
||
function hasStateTransitionServiceImport(content) {
|
||
// state-transition 只能由白名单路径直接耦合;静态、动态与 CommonJS 字符串入口都要按同一路径语义拦截。
|
||
return (
|
||
hasStaticStateTransitionSpecifier(content) ||
|
||
/\bimport\s*\(\s*(?:["'][^"']*state-transition(?:\/index\.js)?["']|`[^`]*state-transition(?:\/index\.js)?`)\s*\)/.test(
|
||
content
|
||
) ||
|
||
/\brequire\s*\(\s*(?:["'][^"']*state-transition(?:\/index\.js)?["']|`[^`]*state-transition(?:\/index\.js)?`)\s*\)/.test(
|
||
content
|
||
) ||
|
||
hasStateTransitionVariableImport(content) ||
|
||
/StateTransitionService/.test(content)
|
||
);
|
||
}
|
||
|
||
function isAllowedStateTransitionImportPath(filePath) {
|
||
// jobs 测试需要 StateTransitionService 创建受 guard 保护的版本夹具;这不是生产调用入口。
|
||
if (stateTransitionImportFixtureTests.has(filePath)) return true;
|
||
if (allowedStateTransitionImportPaths.has(filePath)) return true;
|
||
return allowedStateTransitionImportPrefixes.some((prefix) => filePath.startsWith(prefix));
|
||
}
|
||
|
||
function isAllowedS1AnchorPath(filePath) {
|
||
return (
|
||
filePath.startsWith("apps/api/prisma/") ||
|
||
filePath.startsWith(stateTransitionModulePrefix) ||
|
||
filePath.startsWith(auditModulePrefix) ||
|
||
filePath === historicalPrismaSchemaTest
|
||
);
|
||
}
|
||
|
||
function hasPrismaModelMethodCallExcluding(content, models, methods, excludeCallBody) {
|
||
const scanContent = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content));
|
||
for (const match of prismaModelMethodCallMatchesInStructure(content, scanContent, models, methods)) {
|
||
const openParenIndex = scanContent[match.callStart] === "(" ? match.callStart : scanContent.indexOf("(", match.callStart);
|
||
if (openParenIndex === -1) return true;
|
||
|
||
const closeParenIndex = findMatchingParen(scanContent, openParenIndex);
|
||
if (closeParenIndex === -1) return true;
|
||
|
||
const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1);
|
||
if (!excludeCallBody(callBody)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function isSimpleIdentifierKey(structure, partStart, partEnd, expectedKey) {
|
||
const keyStart = skipWhitespaceForward(structure, partStart);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd);
|
||
if (keyEnd === -1) return false;
|
||
if (structure.slice(keyStart, keyEnd).trim() !== expectedKey) return false;
|
||
return structure[skipWhitespaceForward(structure, keyEnd)] === ":";
|
||
}
|
||
|
||
function objectPropertyValueSpan(structure, partStart, partEnd) {
|
||
const keyStart = skipWhitespaceForward(structure, partStart);
|
||
const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd);
|
||
if (keyEnd === -1) return null;
|
||
|
||
const colonIndex = skipWhitespaceForward(structure, keyEnd);
|
||
if (structure[colonIndex] !== ":") return null;
|
||
|
||
const valueStart = skipWhitespaceForward(structure, colonIndex + 1);
|
||
const valueEnd = skipWhitespaceBackward(structure, partEnd - 1) + 1;
|
||
return valueStart < valueEnd ? { start: valueStart, end: valueEnd } : null;
|
||
}
|
||
|
||
function soleObjectPropertyValueSpan(structure, objectStart, objectEnd, expectedKey) {
|
||
if (structure[objectStart] !== "{" || objectEnd <= objectStart) return null;
|
||
const closeBraceIndex = findMatchingBrace(structure, objectStart);
|
||
if (closeBraceIndex === -1 || closeBraceIndex !== objectEnd - 1) return null;
|
||
|
||
const parts = destructuringParts(structure, objectStart + 1, closeBraceIndex).filter(
|
||
(part) => skipWhitespaceForward(structure, part.start) < skipWhitespaceBackward(structure, part.end - 1) + 1
|
||
);
|
||
if (parts.length !== 1) return null;
|
||
|
||
const [part] = parts;
|
||
if (!isSimpleIdentifierKey(structure, part.start, part.end, expectedKey)) return null;
|
||
return objectPropertyValueSpan(structure, part.start, part.end);
|
||
}
|
||
|
||
function isRunIdStartsWithValue(structure, valueStart, valueEnd) {
|
||
const value = structure.slice(valueStart, valueEnd).trim();
|
||
return value === "runId";
|
||
}
|
||
|
||
function isAllowedJobDeleteCleanupCallBody(filePath, callBody) {
|
||
if (!isTestFile(filePath)) return false;
|
||
|
||
const callStart = skipWhitespaceForward(callBody, 0);
|
||
if (callBody[callStart] !== "(") return false;
|
||
|
||
const callEnd = findMatchingParen(callBody, callStart);
|
||
if (callEnd === -1 || skipWhitespaceForward(callBody, callEnd + 1) !== callBody.length) return false;
|
||
|
||
const argument = nthCallArgumentSource(callBody, callBody, callStart, callEnd, 0);
|
||
if (!argument) return false;
|
||
if (nthCallArgumentSource(callBody, callBody, callStart, callEnd, 1)) return false;
|
||
|
||
const argumentStart = skipWhitespaceForward(callBody, argument.index);
|
||
const argumentEnd = skipWhitespaceBackward(callBody, argument.end - 1) + 1;
|
||
// 测试清理只能是 deleteMany({ where: { id: { startsWith: runId } } });任何 OR/AND/NOT、额外 key、spread/computed 都 fail closed。
|
||
const whereValue = soleObjectPropertyValueSpan(callBody, argumentStart, argumentEnd, "where");
|
||
if (!whereValue) return false;
|
||
|
||
const whereStart = skipWhitespaceForward(callBody, whereValue.start);
|
||
const whereEnd = skipWhitespaceBackward(callBody, whereValue.end - 1) + 1;
|
||
const idValue = soleObjectPropertyValueSpan(callBody, whereStart, whereEnd, "id");
|
||
if (!idValue) return false;
|
||
|
||
const idStart = skipWhitespaceForward(callBody, idValue.start);
|
||
const idEnd = skipWhitespaceBackward(callBody, idValue.end - 1) + 1;
|
||
const startsWithValue = soleObjectPropertyValueSpan(callBody, idStart, idEnd, "startsWith");
|
||
if (!startsWithValue) return false;
|
||
|
||
return isRunIdStartsWithValue(callBody, startsWithValue.start, startsWithValue.end);
|
||
}
|
||
|
||
function hasForbiddenJobPrismaDelete(filePath, content) {
|
||
// 单行 delete 无法表达 runId 前缀清理语义,始终视为 Job 运行态删除;只有 deleteMany 可走测试清理白名单。
|
||
return (
|
||
prismaModelMethodCallMatches(content, ["job"], ["delete"]).length > 0 ||
|
||
hasPrismaModelMethodCallExcluding(content, ["job"], ["deleteMany"], (callBody) => isAllowedJobDeleteCleanupCallBody(filePath, callBody))
|
||
);
|
||
}
|
||
|
||
function hasJobStatusWrite(filePath, content) {
|
||
// Job 创建路径同样会写入运行态字段,S1 必须统一收敛到 JobExecutionStore。
|
||
return (
|
||
hasPrismaModelMethodCallContaining(content, ["job"], jobPrismaWriteMethods, { test: hasForbiddenJobWriteCallBody }) ||
|
||
hasForbiddenJobPrismaDelete(filePath, content) ||
|
||
hasRawJobStatusWrite(content)
|
||
);
|
||
}
|
||
|
||
function hasForbiddenJobStatusWrite(filePath, content) {
|
||
// SQL 文件走原始 SQL 扫描;源码文件继续复用 Prisma 与 raw 调用扫描。
|
||
if (filePath.endsWith(".sql")) return hasForbiddenRawJobStatusWrite(filePath, content);
|
||
return hasJobStatusWrite(filePath, content);
|
||
}
|
||
|
||
function isAllowedJobExecutionStorePath(filePath) {
|
||
return filePath === jobExecutionStorePath;
|
||
}
|
||
|
||
async function collectFiles(root, relativeDir) {
|
||
const absoluteDir = path.join(root, relativeDir);
|
||
const entries = await readdir(absoluteDir, { withFileTypes: true }).catch((error) => {
|
||
if (error && error.code === "ENOENT") return [];
|
||
throw error;
|
||
});
|
||
const files = [];
|
||
|
||
for (const entry of entries) {
|
||
const relativePath = normalizePath(path.join(relativeDir, entry.name));
|
||
if (entry.isDirectory()) {
|
||
if (ignoredScanSegments.has(entry.name)) continue;
|
||
files.push(...(await collectFiles(root, relativePath)));
|
||
continue;
|
||
}
|
||
|
||
if (entry.isFile() && sourceLikeFilePattern.test(entry.name) && !isIgnoredScanPath(relativePath)) {
|
||
files.push({
|
||
path: relativePath,
|
||
content: await readFile(path.join(root, relativePath), "utf8")
|
||
});
|
||
}
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
async function collectWorkspaceFiles(root) {
|
||
const files = [];
|
||
for (const scanRoot of scanRoots) {
|
||
files.push(...(await collectFiles(root, scanRoot)));
|
||
}
|
||
return files;
|
||
}
|
||
|
||
function pushViolation(violations, filePath, reason, detail) {
|
||
violations.push({ path: filePath, reason, detail });
|
||
}
|
||
|
||
function normalizedPathSegments(filePath) {
|
||
return normalizePath(filePath)
|
||
.replace(/^\/+|\/+$/g, "")
|
||
.split("/")
|
||
.filter((segment) => segment.length > 0);
|
||
}
|
||
|
||
function hasS2ApprovedTermDenyPath(filePath) {
|
||
const normalizedFilePath = `/${normalizePath(filePath).replace(/^\/+|\/+$/g, "")}/`;
|
||
if (s2ApprovedTermDenyPathFragments.some((fragment) => normalizedFilePath.includes(fragment))) return true;
|
||
return normalizedPathSegments(filePath).some((segment) => s2ApprovedTermDenyPathSegments.has(segment));
|
||
}
|
||
|
||
function hasS2ApprovedTermAllowPath(filePath) {
|
||
if (s2ApprovedTermAllowExactPaths.has(filePath)) return true;
|
||
return s2ApprovedTermAllowPathPrefixes.some((prefix) => filePath.startsWith(prefix));
|
||
}
|
||
|
||
function isAllowedS2ApprovedTermReference(filePath, term) {
|
||
if (!s2ApprovedTerms.has(term)) return false;
|
||
if (hasS2ApprovedTermDenyPath(filePath)) return false;
|
||
|
||
// S2 approved 术语只能出现在 Creator Workbench、AI design、GameIR 和共享合同边界内。
|
||
// 不从 denylist 全局删除这些词,避免 simulation/runtime/conversion/feed 等后续阶段路径借名绕过门禁。
|
||
return hasS2ApprovedTermAllowPath(filePath);
|
||
}
|
||
|
||
function shouldSkipLaterPhaseTermViolation(filePath, term, phase) {
|
||
if (phase === "S2" && isAllowedS2ApprovedTermReference(filePath, term)) return true;
|
||
if (phase === "S1") return false;
|
||
if (phase === "S2") return false;
|
||
throw new Error(`Unsupported scope phase: ${phase}`);
|
||
}
|
||
|
||
function findLaterPhaseScopeViolations(file, violations, phase) {
|
||
for (const term of laterPhaseDeniedTerms) {
|
||
// 路径名本身不能提供 validateContract 调用上下文;harness-client 测试也不能按路径泛化放行。
|
||
if (pathContainsLaterPhaseTerm(file.path, term)) {
|
||
if (shouldSkipLaterPhaseTermViolation(file.path, term, phase)) continue;
|
||
pushViolation(violations, file.path, "LATER_PHASE_SCOPE_LEAK", term);
|
||
continue;
|
||
}
|
||
|
||
let hasContentViolation = false;
|
||
for (const contentTerm of contentDeniedTermVariants(term)) {
|
||
let index = file.content.indexOf(contentTerm);
|
||
while (index !== -1) {
|
||
if (!isAllowedHarnessValidationContractReferenceAt(file.path, file.content, contentTerm, index)) {
|
||
if (shouldSkipLaterPhaseTermViolation(file.path, term, phase)) break;
|
||
pushViolation(violations, file.path, "LATER_PHASE_SCOPE_LEAK", term);
|
||
hasContentViolation = true;
|
||
break;
|
||
}
|
||
index = file.content.indexOf(contentTerm, index + contentTerm.length);
|
||
}
|
||
if (hasContentViolation) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
function findHarnessClientContractReferenceViolations(file, violations) {
|
||
if (!isHarnessClientTestPath(file.path)) return;
|
||
|
||
for (const term of harnessClientContractWatchTerms) {
|
||
let index = file.content.indexOf(term);
|
||
while (index !== -1) {
|
||
if (!isAllowedHarnessClientContractReferenceAt(file.path, file.content, term, index)) {
|
||
pushViolation(violations, file.path, "HARNESS_CONTRACT_REFERENCE_OUTSIDE_VALIDATION", term);
|
||
break;
|
||
}
|
||
index = file.content.indexOf(term, index + term.length);
|
||
}
|
||
}
|
||
}
|
||
|
||
function s1AnchorTermsForPhase(phase) {
|
||
if (phase === "S1") return s1AnchorTerms;
|
||
if (phase === "S2") {
|
||
// S2 会启用 S1 已存在的创作 Agent anchor;审核/生命周期 anchor 仍不能进入 S2 业务实现。
|
||
return s1AnchorTerms.filter((term) => !s2ApprovedS1AnchorTerms.has(term));
|
||
}
|
||
throw new Error(`Unsupported scope phase: ${phase}`);
|
||
}
|
||
|
||
function findS1AnchorScopeViolations(file, violations, phase) {
|
||
if (isAllowedS1AnchorPath(file.path)) return;
|
||
for (const term of s1AnchorTermsForPhase(phase)) {
|
||
if (file.content.includes(term)) {
|
||
pushViolation(violations, file.path, "S1_ANCHOR_SCOPE_MISUSE", term);
|
||
}
|
||
}
|
||
}
|
||
|
||
function findAuditBoundaryViolations(file, violations) {
|
||
if (isGeneratedPrismaFile(file.path)) return;
|
||
if (hasForbiddenAuditMutation(file.path, file.content)) {
|
||
pushViolation(violations, file.path, "AUDIT_UPDATE_DELETE_FORBIDDEN", "auditLog update/delete/upsert");
|
||
}
|
||
}
|
||
|
||
function findDynamicRawSqlViolations(file, violations) {
|
||
if (isGeneratedPrismaFile(file.path)) return;
|
||
if (hasDynamicIdentifierRawSqlWrite(file.content)) {
|
||
pushViolation(violations, file.path, "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", "dynamic raw SQL write identifier");
|
||
}
|
||
}
|
||
|
||
function findStateBoundaryViolations(file, violations) {
|
||
if (isHistoricalTestFile(file.path) || isGeneratedPrismaFile(file.path)) return;
|
||
|
||
if (file.path.startsWith("apps/worker/")) {
|
||
if (/@prisma\/client|generated\/prisma|PrismaClient/.test(file.content)) {
|
||
pushViolation(violations, file.path, "WORKER_DB_IMPORT_FORBIDDEN", "Prisma import in worker");
|
||
}
|
||
if (/state-transition|StateTransitionService|app\.state_transition_guard|set_config\s*\(|SET\s+LOCAL/i.test(file.content)) {
|
||
pushViolation(violations, file.path, "WORKER_STATE_TRANSITION_IMPORT_FORBIDDEN", "state transition coupling in worker");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (hasStateTransitionServiceImport(file.content) && !isAllowedStateTransitionImportPath(file.path)) {
|
||
pushViolation(violations, file.path, "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", "state-transition service import");
|
||
}
|
||
|
||
if (file.path.startsWith(stateTransitionModulePrefix)) return;
|
||
|
||
if (file.path.startsWith(prismaMigrationPrefix)) {
|
||
// migration 允许安装 trigger/current_setting guard,但不允许写运行时 guard setter。
|
||
if (hasForbiddenMigrationGuardSetter(file.content)) {
|
||
pushViolation(violations, file.path, "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION", "migration runtime guard setter");
|
||
}
|
||
if (hasGuardedPrismaWrite(file.path, file.content)) {
|
||
pushViolation(violations, file.path, "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", "migration guarded model write");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (hasGuardedPrismaWrite(file.path, file.content)) {
|
||
pushViolation(violations, file.path, "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", "guarded model write");
|
||
}
|
||
if (hasRuntimeGuardSetter(file.content)) {
|
||
pushViolation(violations, file.path, "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION", "runtime guard setter");
|
||
}
|
||
}
|
||
|
||
function findJobExecutionBoundaryViolations(file, violations) {
|
||
const scanContent = isAllowedJobExecutionStorePath(file.path) ? maskJobExecutionStoreClassBodies(file.content) : file.content;
|
||
if (hasForbiddenJobStatusWrite(file.path, scanContent)) {
|
||
pushViolation(violations, file.path, "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", "Job status write");
|
||
}
|
||
}
|
||
|
||
function findViolationsInFiles(files) {
|
||
return findScopeViolationsInFiles(files, "S1");
|
||
}
|
||
|
||
function findScopeViolationsInFiles(files, phase) {
|
||
const violations = [];
|
||
for (const file of files) {
|
||
if (isIgnoredScanPath(file.path) || isGeneratedPrismaFile(file.path)) continue;
|
||
findLaterPhaseScopeViolations(file, violations, phase);
|
||
findHarnessClientContractReferenceViolations(file, violations);
|
||
findS1AnchorScopeViolations(file, violations, phase);
|
||
findAuditBoundaryViolations(file, violations);
|
||
findStateBoundaryViolations(file, violations);
|
||
findDynamicRawSqlViolations(file, violations);
|
||
findJobExecutionBoundaryViolations(file, violations);
|
||
}
|
||
return violations;
|
||
}
|
||
|
||
async function findS1ScopeViolations(root) {
|
||
return findScopeViolations(root, "S1");
|
||
}
|
||
|
||
async function findScopeViolations(root, phase) {
|
||
return findScopeViolationsInFiles(await collectWorkspaceFiles(root), phase);
|
||
}
|
||
|
||
async function withFixture(files, callback) {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "huijing-s1-scope-"));
|
||
try {
|
||
for (const [relativePath, content] of Object.entries(files)) {
|
||
const absolutePath = path.join(root, relativePath);
|
||
await mkdir(path.dirname(absolutePath), { recursive: true });
|
||
await writeFile(absolutePath, content);
|
||
}
|
||
return await callback(root);
|
||
} finally {
|
||
await rm(root, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
function assertHasReason(violations, reason, label) {
|
||
if (!violations.some((violation) => violation.reason === reason)) {
|
||
throw new Error(`${label}: expected ${reason}`);
|
||
}
|
||
}
|
||
|
||
function assertNoViolations(violations, label) {
|
||
if (violations.length > 0) {
|
||
const details = violations.map(formatViolation).join("; ");
|
||
throw new Error(`${label}: expected no violations, got ${details}`);
|
||
}
|
||
}
|
||
|
||
function assertThrows(fn, label) {
|
||
try {
|
||
fn();
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
throw new Error(`${label}: expected throw`);
|
||
}
|
||
|
||
function formatViolation(violation) {
|
||
return `${violation.path}: ${violation.reason}${violation.detail ? ` (${violation.detail})` : ""}`;
|
||
}
|
||
|
||
function parsePhaseArg(argv) {
|
||
const phaseFlagIndexes = argv.flatMap((arg, index) => (arg === "--phase" ? [index] : []));
|
||
if (phaseFlagIndexes.length !== 1) {
|
||
throw new Error("Expected exactly one --phase S1|S2");
|
||
}
|
||
|
||
const [phaseFlagIndex] = phaseFlagIndexes;
|
||
if (phaseFlagIndex === argv.length - 1) {
|
||
throw new Error("Missing required --phase S1|S2");
|
||
}
|
||
|
||
const phase = argv[phaseFlagIndex + 1]?.toUpperCase();
|
||
if (!supportedPhases.has(phase)) {
|
||
throw new Error(`Unsupported --phase value: ${argv[phaseFlagIndex + 1] ?? ""}`);
|
||
}
|
||
|
||
return phase;
|
||
}
|
||
|
||
async function runSelfTests() {
|
||
assertThrows(() => parsePhaseArg(["--phase", "S2", "--phase", "S1"]), "duplicate phase flag");
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/creator/index.ts": 'export const route = "creation-agent/messages";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "later-phase app route");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/index.spec.ts":
|
||
'createHarnessClient().validateContract("MiniGameCodeConversion", { id: "fixture" });'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "harness-client S0 contract fixture reference");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/probe.spec.ts":
|
||
'createHarnessClient(); const businessModelName = "MiniGameProject";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "harness-client non-validation S0 contract probe");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/bad-game-ir.spec.ts": 'const contractName = "GameIR";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "harness-client non-validation GameIR contract name");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/bad-validation-report.spec.ts": 'const contractName = "ValidationReport";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(
|
||
violations,
|
||
"HARNESS_CONTRACT_REFERENCE_OUTSIDE_VALIDATION",
|
||
"harness-client non-validation ValidationReport contract name"
|
||
);
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/index.spec.ts":
|
||
'createHarnessClient().validateContract("GameIR", { id: "fixture" });'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "harness-client GameIR validation reference");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/index.spec.ts":
|
||
'createHarnessClient().validateContract("ValidationReport", { id: "fixture" });'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "harness-client ValidationReport validation reference");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/index.spec.ts": 'expect(args[2]).toBe("GameIR");'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "isolated harness-client args[2] contract assertion");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"packages/harness-client/src/index.spec.ts": `
|
||
const runner = async (_file, args) => {
|
||
expect(args.slice(0, 2)).toEqual([realHarnessCli, "--contract"]);
|
||
expect(args[2]).toBe("GameIR");
|
||
};
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "harness-client args[2] assertion with runner contract context");
|
||
}
|
||
);
|
||
|
||
const harnessClientPathOnlyLaterPhaseFixtures = [
|
||
"packages/harness-client/src/game-ir.spec.ts",
|
||
"packages/harness-client/src/game-ir/index.spec.ts",
|
||
"packages/harness-client/src/game-ir-artifact.spec.ts",
|
||
"packages/harness-client/src/validation-report.spec.ts"
|
||
];
|
||
|
||
for (const filePath of harnessClientPathOnlyLaterPhaseFixtures) {
|
||
await withFixture(
|
||
{
|
||
[filePath]: "export const harmless = true;"
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", `harness-client path-only later-phase leak ${filePath}`);
|
||
}
|
||
);
|
||
}
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/index.ts": 'const contractName = "MiniGameCodeConversion";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "API business later-phase contract");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/agent.ts": "export class MainCreationAgentSessionController {}"
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "S1_ANCHOR_SCOPE_MISUSE", "S1 anchor business misuse");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/prisma/migrations/20260601000000_guard/migration.sql": `
|
||
CREATE OR REPLACE FUNCTION app_require_state_transition_guard()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
IF current_setting('app.state_transition_guard', true) IS DISTINCT FROM 'on' THEN
|
||
RAISE EXCEPTION 'guard required';
|
||
END IF;
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
CREATE TRIGGER "LifecycleEvent_a_guard_insert" BEFORE INSERT ON "LifecycleEvent"
|
||
FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard();
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "migration trigger/current_setting guard");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/prisma/migrations/20260602010101_install_audit_guard/migration.sql": `
|
||
CREATE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "plain AuditLog append-only function install migration");
|
||
}
|
||
);
|
||
|
||
const failingFixtures = [
|
||
{
|
||
label: "migration set_config guard setter",
|
||
reason: "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260601000001_bad_guard/migration.sql":
|
||
"SELECT set_config('app.state_transition_guard', 'on', true);"
|
||
}
|
||
},
|
||
{
|
||
label: "app SET LOCAL guard setter",
|
||
reason: "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/bad.ts": 'await db.$executeRawUnsafe("SET LOCAL app.state_transition_guard = on");'
|
||
}
|
||
},
|
||
{
|
||
label: "app raw SQL guard setter",
|
||
reason: "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/raw.ts":
|
||
'await db.$executeRawUnsafe("SELECT set_config(\'app.state_transition_guard\', \'on\', true)");'
|
||
}
|
||
},
|
||
{
|
||
label: "API spec raw SQL guard setter",
|
||
reason: "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/bad.spec.ts":
|
||
'await db.$executeRawUnsafe("SELECT set_config(\'app.state_transition_guard\', \'on\', true)");'
|
||
}
|
||
},
|
||
{
|
||
label: "API spec SET LOCAL guard setter",
|
||
reason: "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/bad-set-local.spec.ts":
|
||
'await db.$executeRawUnsafe("SET LOCAL app.state_transition_guard = on");'
|
||
}
|
||
},
|
||
{
|
||
label: "audit update/delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit.ts":
|
||
"await db.auditLog.update({ where: { id }, data: {} }); await db.auditLog.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit bracket model update outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-bracket-model.ts":
|
||
'await db["auditLog"].update({ where: { id }, data: {} });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit computed bracket model delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-computed-bracket-model.ts":
|
||
'await db["audit" + "Log"].delete({ where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit bracket method delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-bracket-method.ts":
|
||
'await db.auditLog["delete"]({ where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit bracket model and method delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-bracket-model-method.ts":
|
||
'await db["auditLog"]["delete"]({ where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit optional delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-optional-delete.ts": "await db.auditLog?.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit optional bracket delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-optional-bracket-delete.ts":
|
||
'await db["auditLog"]?.["delete"]({ where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit parenthesized delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-parenthesized-delete.ts":
|
||
"await (db.auditLog).delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit delegate alias delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alias-delete.ts":
|
||
"const logs = db.auditLog; await logs.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit destructured delegate alias delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-destructured-alias-delete.ts":
|
||
"const { auditLog: logs } = db; await logs.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit quoted destructured delegate alias delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-quoted-destructured-alias-delete.ts":
|
||
'const { "auditLog": logs } = db; await logs.delete({ where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "audit assigned delegate alias delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-assigned-alias-delete.ts": `
|
||
let logs;
|
||
logs = db.auditLog;
|
||
await logs.delete({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "audit Function.prototype.call delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-function-call-delete.ts":
|
||
"Function.prototype.call.call(db.auditLog.delete, db.auditLog, { where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit Function.prototype.call alias delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-function-call-alias-delete.ts": `
|
||
const c = Function.prototype.call;
|
||
c.call(db.auditLog.delete, db.auditLog, { where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "audit update/delete in API spec outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit.spec.ts": "await db.auditLog.update({ where: { id }, data: {} });"
|
||
}
|
||
},
|
||
{
|
||
label: "audit update/delete inside audit module implementation",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/audit/bad.ts":
|
||
"await db.auditLog.update({ where: { id }, data: {} }); await db.auditLog.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition service import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state.ts": 'import { StateTransitionService } from "../state-transition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition side-effect import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-side-effect.ts": 'import "../state-transition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition escaped side-effect import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-side-effect-escaped.ts": 'import "../state-\\u0074ransition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition escaped named import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-named-escaped.ts":
|
||
'import { StateTransitionService } from "../state-\\u0074ransition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition escaped export from outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-export-escaped.ts":
|
||
'export { StateTransitionService } from "../state-\\u0074ransition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic.ts": 'await import("../state-transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition backtick dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-backtick.ts": 'await import(`../state-transition/index.js`);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition parenthesized literal dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-parenthesized-literal.ts":
|
||
'await import(("../state-transition/index.js"));'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition parenthesized backtick dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-parenthesized-backtick.ts":
|
||
"await import((`../state-transition/index.js`));"
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition concatenated direct dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-concatenated-direct.ts":
|
||
'await import("../state-transition" + "/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition parenthesized concatenated direct dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-parenthesized-concatenated-direct.ts":
|
||
'await import(("../state-transition" + "/index.js"));'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require.ts": 'require("../state-transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition parenthesized literal require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-parenthesized-literal.ts":
|
||
'require(("../state-transition/index.js"));'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition concatenated direct require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-concatenated-direct.ts":
|
||
'require("../state-" + "transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition parenthesized concatenated direct require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-parenthesized-concatenated-direct.ts":
|
||
'require(("../state-" + "transition/index.js"));'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition variable dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-variable.ts":
|
||
'const mod = "../state-transition/index.js"; await import(mod);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition recursive variable dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-dynamic-recursive-variable.ts":
|
||
'const stateTransitionPath = "../state-transition/index.js"; const mod = stateTransitionPath; await import(mod);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition variable require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-variable.ts":
|
||
'const mod = "../state-transition/index.js"; require(mod);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition concatenated variable require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-concatenated-variable.ts":
|
||
'const base = "../state-transition"; const mod = base + "/index.js"; require(mod);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition concatenated variable dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/state-dynamic-concatenated-variable.ts":
|
||
'const base = "../state-"; await import(base + "transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition concatenated variable direct require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/state-require-direct-concatenated-variable.ts":
|
||
'const base = "../state-"; require(base + "transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition static template expression dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/state-dynamic-static-template.ts":
|
||
'await import(`../${"state-transition"}/index.js`);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition static template variable dynamic import outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/state-dynamic-static-template-variable.ts":
|
||
'const seg = "state-transition"; await import(`../${seg}/index.js`);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition destructured require alias outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/state-destructured-require-alias.ts":
|
||
'const { require: load } = module; load("../state-transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition backtick require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-backtick.ts": 'require(`../state-transition/index.js`);'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition aliased require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-alias.ts":
|
||
'const r = require; return r("../state-transition/index.js");'
|
||
}
|
||
},
|
||
{
|
||
label: "state-transition assigned aliased require outside allowed path",
|
||
reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/assets/state-require-assigned-alias.ts": `
|
||
let r;
|
||
r = require;
|
||
r("../state-transition/index.js");
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma import in worker",
|
||
reason: "WORKER_DB_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/worker/src/index.ts": 'import { PrismaClient } from "@prisma/client";'
|
||
}
|
||
},
|
||
{
|
||
label: "worker state-transition import",
|
||
reason: "WORKER_STATE_TRANSITION_IMPORT_FORBIDDEN",
|
||
files: {
|
||
"apps/worker/src/state.ts": 'import { StateTransitionService } from "../../api/src/modules/state-transition/index.js";'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job.ts": "await db.job.update({ where: { id }, data: { status: 'failed' } });"
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delete.ts": "await db.job.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job deleteMany outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delete-many.ts": 'await db.job.deleteMany({ where: { status: "running" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "test Job delete runId cleanup outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-cleanup.spec.ts": `
|
||
const runId = "scope-test";
|
||
await db.job.delete({ where: { id: { startsWith: runId } } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "test Job deleteMany OR cleanup outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-cleanup.spec.ts": `
|
||
const runId = "scope-test";
|
||
await db.job.deleteMany({ where: { OR: [{ id: { startsWith: runId } }, { status: "running" }] } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job bracket model status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-model.ts":
|
||
'await db["job"].update({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job bracket method status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-method.ts":
|
||
'await db.job["update"]({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job bracket model and method status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-model-method.ts":
|
||
'await db["job"]["update"]({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job optional status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-optional-update.ts":
|
||
"await db.job?.update({ where: { id }, data: { status } });"
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job optional bracket status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-optional-bracket-update.ts":
|
||
'await db["job"]?.["update"]({ where: { id }, data: { status } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job parenthesized delegate status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-parenthesized-update.ts":
|
||
'await (db.job).update({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job delegate alias status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-alias-update.ts":
|
||
"const jobs = db.job; await jobs.update({ where: { id }, data: { status } });"
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job delegate alias delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-alias-delete.ts": "const jobs = db.job; await jobs.delete({ where: { id } });"
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job destructured delegate alias status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-destructured-alias-update.ts":
|
||
'const { job } = db; await job.update({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job assigned delegate alias status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-assigned-alias-update.ts": `
|
||
let jobs;
|
||
jobs = db.job;
|
||
await jobs.update({ where: { id }, data: { status: "running" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write with comment paren outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-comment-paren.ts": `
|
||
await db.job.update({ // )
|
||
where: { id },
|
||
data: { status: "failed" }
|
||
});
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write with data identifier outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-data-identifier.ts":
|
||
'const patch = { status: "failed" }; await db.job.update({ where: { id }, data: patch });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write with quoted keys outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-quoted.ts":
|
||
'await db.job.update({ where: { id }, data: { "status": "failed", \'attempts\': 1, [`nextRetryAt`]: null } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job lease token write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-lease-token.ts":
|
||
'await db.job.update({ where: { id }, data: { leaseToken: "stolen" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job timeoutAt write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-timeout-at.ts":
|
||
'await db.job.update({ where: { id }, data: { timeoutAt: new Date() } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job runtime write with dynamic computed key outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-computed.ts":
|
||
'const field = "status"; await db.job.update({ where: { id }, data: { [field]: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status create outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-create.ts":
|
||
'await db.job.create({ data: { projectId, actorId, status: "queued", attempts: 0, nextRetryAt: null } });'
|
||
}
|
||
},
|
||
{
|
||
label: "variableized Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-variable-data.ts":
|
||
'const data = { status: "failed" }; await db.job.update({ where: { id }, data });'
|
||
}
|
||
},
|
||
{
|
||
label: "spread Job data write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-spread-data.ts":
|
||
'const patch = { attempts: 1 }; await db.job.update({ where: { id }, data: { ...patch } });'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-status.ts":
|
||
`await db.$executeRawUnsafe("UPDATE \\"Job\\" SET \\"status\\" = 'failed' WHERE id = $1", id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job identity-escaped keyword status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-identity-escaped-keyword-status.ts":
|
||
'await db.$executeRawUnsafe("\\U\\P\\D\\A\\T\\E \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job unicode-escaped table status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-unicode-table.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"\\u004aob\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job unicode quoted identifier status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-unicode-quoted-identifier-status.ts":
|
||
`await db.$executeRawUnsafe('UPDATE U&"J\\\\006Fb" SET U&"stat\\\\0075s" = $1 WHERE id = $2', status, id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job custom UESCAPE unicode quoted identifier status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-unicode-quoted-custom-uescape-status.ts":
|
||
`await db.$executeRawUnsafe('UPDATE U&"J!006Fb" UESCAPE \\'!\\' SET U&"stat!0075s" UESCAPE \\'!\\' = $1 WHERE id = $2', status, id);`
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-optional-status.ts":
|
||
'await db.$executeRawUnsafe?.("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "parenthesized raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-parenthesized-status.ts":
|
||
'await (db.$executeRawUnsafe)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "parenthesized optional bracket raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-parenthesized-optional-bracket-status.ts":
|
||
'await (db?.["$executeRawUnsafe"])("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "bound raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-bind-status.ts":
|
||
'await db.$executeRawUnsafe.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "optional bound raw SQL Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-optional-bind-status.ts":
|
||
'await db.$executeRawUnsafe?.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "no-semicolon bound raw SQL Job status alias outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-bound-alias-no-semicolon.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db)
|
||
await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id)
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "no-semicolon optional bound raw SQL Job status alias outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-optional-bound-alias-no-semicolon.ts": `
|
||
const execRaw = db.$executeRawUnsafe?.bind(db)
|
||
await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id)
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write with comment paren outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-comment-paren.ts": `
|
||
await db.$executeRawUnsafe( // )
|
||
"UPDATE \\"Job\\" SET \\"status\\" = 'failed' WHERE id = $1",
|
||
id
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job attempts write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-attempts.ts":
|
||
'await db.$executeRawUnsafe("UPDATE Job SET attempts = attempts + 1 WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job nextRetryAt write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-next-retry.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"Job\\" SET \\"nextRetryAt\\" = NULL WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job insert status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-insert-status.ts":
|
||
'await db.$executeRawUnsafe("INSERT INTO \\"Job\\" (\\"id\\", \\"status\\") VALUES ($1, \'queued\')", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job lease fields update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-lease.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"Job\\" SET \\"leaseToken\\" = $1, \\"lockVersion\\" = \\"lockVersion\\" + 1 WHERE id = $2", token, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job multi-column SET outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-multi-column-set.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"Job\\" SET (\\"status\\", \\"attempts\\") = (\'failed\', 1) WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job MERGE update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-merge.ts":
|
||
'await db.$executeRawUnsafe("MERGE INTO \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job ONLY MERGE update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-only-merge.ts":
|
||
'await db.$executeRawUnsafe("MERGE INTO ONLY \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job ONLY block-comment MERGE update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-only-block-comment-merge.ts":
|
||
'await db.$executeRawUnsafe("MERGE/*x*/INTO ONLY \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job nested block-comment update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-nested-block-comment-update.ts":
|
||
'await db.$executeRawUnsafe("UPDATE/* outer /* inner */ still outer */\\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status update after line-comment-looking string literal",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-status-after-string-line-comment.ts":
|
||
`await db.$executeRawUnsafe("SELECT '-- not a comment'; UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job multi-table truncate outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-multi-truncate.ts":
|
||
'await db.$executeRawUnsafe("TRUNCATE TABLE \\"Other\\", \\"Job\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job status update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000000_bad_job_update/migration.sql":
|
||
'UPDATE "Job" SET "status" = \'failed\' WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job ONLY inheritance status update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000006_bad_job_only_inheritance_update/migration.sql":
|
||
'UPDATE ONLY "Job" * SET "status" = \'failed\' WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job unicode quoted identifier status update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000007_bad_job_unicode_quoted_update/migration.sql":
|
||
'UPDATE U&"J\\006Fb" SET U&"stat\\0075s" = \'failed\' WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job status update after line-comment-looking string literal",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000004_bad_job_update_after_string_line_comment/migration.sql":
|
||
`SELECT '-- not a comment'; UPDATE "Job" SET "status" = 'failed' WHERE id = 'x';`
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job MERGE update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000001_bad_job_merge/migration.sql":
|
||
'MERGE INTO "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job ONLY MERGE update outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000005_bad_job_only_merge/migration.sql":
|
||
'MERGE INTO ONLY "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job multi-table truncate outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602000003_bad_job_truncate/migration.sql":
|
||
'TRUNCATE TABLE "Other", "Job";'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job timeout insert outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-timeout.ts":
|
||
'await db.$executeRawUnsafe("INSERT INTO Job (id, timeoutAt) VALUES ($1, NOW())", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL schema-qualified Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-schema-qualified.ts":
|
||
'await db.$executeRawUnsafe("UPDATE public.\\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL aliased Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-alias.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"Job\\" j SET \\"status\\" = $1 WHERE j.id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL ONLY Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-only.ts":
|
||
'await db.$executeRawUnsafe("UPDATE ONLY \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL ONLY Job inheritance status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-only-inheritance.ts":
|
||
'await db.$executeRawUnsafe("UPDATE ONLY \\"Job\\" * SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql variable Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-prisma-sql-variable.ts":
|
||
'const query = Prisma.sql`UPDATE public."Job" SET "status" = ${status} WHERE id = ${id}`; await db.$executeRaw(query);'
|
||
}
|
||
},
|
||
{
|
||
label: "concatenated SQL variable Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-concatenated-variable.ts":
|
||
'const query = "UPDATE " + "\\"Job\\"" + " SET " + "\\"status\\"" + " = $1 WHERE id = $2"; await db.$executeRawUnsafe(query, status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through intermediate constants outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-intermediate-constants.ts": `
|
||
const tableName = '"Job"';
|
||
const fieldName = '"status"';
|
||
const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through unicode-escaped intermediate constants outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-unicode-intermediate-constants.ts": `
|
||
const tableName = '"\\u004aob"';
|
||
const fieldName = '"status"';
|
||
const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through dynamic table variable outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const fieldName = '"status"';
|
||
const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "parenthesized raw SQL dynamic write outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-parenthesized-dynamic-table.ts": `
|
||
const table = process.env.TABLE;
|
||
const query = "UPDATE " + table + " SET status = $1";
|
||
await (db.$executeRawUnsafe)(query, status);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job ONLY inheritance write through dynamic table variable outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-only-inheritance-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const query = "UPDATE ONLY " + tableName + " * j SET status = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job write through identity-escaped dynamic table variable outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-identity-escaped-dynamic-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const query = "\\U\\P\\D\\A\\T\\E " + tableName + " SET status = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job write through dynamic table function call outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-table-call.ts": `
|
||
const query = "UPDATE " + resolveTable() + " SET status = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job write through dynamic field function call outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-field-call.ts": `
|
||
const query = "UPDATE Job SET " + resolveField() + " = $1 WHERE id = $2";
|
||
await db.$executeRawUnsafe(query, value, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic merge table outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-merge-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const query = "MERGE INTO " + tableName + " j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET status = $1";
|
||
await db.$executeRawUnsafe(query, status);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic merge only table outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-dynamic-merge-only-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const query = "MERGE INTO ONLY " + tableName + " j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET status = $1";
|
||
await db.$executeRawUnsafe(query, status);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql Job status write through Prisma.raw intermediate constants outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-prisma-raw-intermediate-constants.ts": `
|
||
const tableName = '"Job"';
|
||
const fieldName = '"status"';
|
||
const query = Prisma.sql\`UPDATE \${Prisma.raw(tableName)} SET \${Prisma.raw(fieldName)} = \${status} WHERE id = \${id}\`;
|
||
await db.$executeRaw(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql Job status write through dynamic Prisma.raw table outside JobExecutionStore",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-prisma-raw-dynamic-table.ts": `
|
||
const tableName = process.env.JOB_TABLE ?? '"Job"';
|
||
const fieldName = '"status"';
|
||
const query = Prisma.sql\`UPDATE \${Prisma.raw(tableName)} SET \${Prisma.raw(fieldName)} = \${status} WHERE id = \${id}\`;
|
||
await db.$executeRaw(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog update outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-update.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"AuditLog\\" SET \\"action\\" = $1 WHERE id = $2", action, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog inheritance update outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-inheritance-update.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"AuditLog\\" * SET \\"action\\" = $1 WHERE id = $2", action, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-delete.ts":
|
||
'await db.$executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog identity-escaped keyword delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-identity-escaped-delete.ts":
|
||
'await db.$executeRawUnsafe("\\D\\E\\L\\E\\T\\E FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog hex-escaped delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-hex-delete.ts":
|
||
'await db.$executeRawUnsafe("DELETE FROM \\"\\x41uditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog unicode quoted identifier delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-unicode-quoted-identifier-delete.ts":
|
||
`await db.$executeRawUnsafe('DELETE FROM U&"Audit\\\\004Cog" WHERE id = $1', id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog custom UESCAPE unicode quoted identifier delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-unicode-quoted-custom-uescape-delete.ts":
|
||
`await db.$executeRawUnsafe('DELETE FROM U&"Audit!004Cog" UESCAPE \\'!\\' WHERE id = $1', id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete after line-comment-looking string literal",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-delete-after-string-line-comment.ts":
|
||
`await db.$executeRawUnsafe("SELECT '-- not a comment'; DELETE FROM \\"AuditLog\\" WHERE id = $1", id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete after block-comment-looking string literal",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-delete-after-string-block-comment.ts":
|
||
`await db.$executeRawUnsafe("SELECT '/* not a comment */'; DELETE FROM \\"AuditLog\\" WHERE id = $1", id);`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog upsert outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-upsert.ts":
|
||
'await db.$executeRawUnsafe("INSERT INTO \\"AuditLog\\" (\\"id\\", \\"action\\") VALUES ($1, $2) ON CONFLICT (\\"id\\") DO UPDATE SET \\"action\\" = EXCLUDED.\\"action\\"", id, action);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog block-comment upsert outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-block-comment-upsert.ts":
|
||
'await db.$executeRawUnsafe("INSERT/*x*/INTO \\"AuditLog\\" (\\"id\\", \\"action\\") VALUES ($1, $2) ON/*x*/CONFLICT (\\"id\\") DO/*x*/UPDATE SET \\"action\\" = EXCLUDED.\\"action\\"", id, action);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog merge outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-merge.ts":
|
||
'await db.$executeRawUnsafe("MERGE INTO \\"AuditLog\\" a USING \\"AuditLogPatch\\" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET \\"action\\" = p.\\"action\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog block-comment merge outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-block-comment-merge.ts":
|
||
'await db.$executeRawUnsafe("MERGE/*x*/INTO \\"AuditLog\\" a USING \\"AuditLogPatch\\" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET \\"action\\" = p.\\"action\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog block-comment delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-block-comment-delete.ts":
|
||
'await db.$executeRawUnsafe("DELETE/*x*/FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog block-comment update outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-block-comment-update.ts":
|
||
'await db.$executeRawUnsafe("UPDATE/*x*/\\"AuditLog\\" SET \\"action\\" = $1 WHERE id = $2", action, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through dot whitespace executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-dot-whitespace-delete.ts":
|
||
'await db. $executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional receiver executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-receiver-delete.ts":
|
||
'await db?.$executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-delete.ts":
|
||
'await db["$executeRawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional bracket executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-delete.ts":
|
||
'await db?.["$executeRawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-delete.ts":
|
||
'await db["$execute" + "RawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through three-part computed executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-three-part-delete.ts":
|
||
'await db["$execute" + "Raw" + "Unsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed executor name alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-alias-delete.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
await db[rawName]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional computed executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-computed-delete.ts":
|
||
'await db?.["$execute" + "RawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through parenthesized executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-parenthesized-delete.ts":
|
||
'await (db.$executeRawUnsafe)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through parenthesized bracket executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-parenthesized-bracket-delete.ts":
|
||
'await (db["$executeRawUnsafe"])("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-call-delete.ts":
|
||
'await db["$executeRawUnsafe"].call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional bracket call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-call-delete.ts":
|
||
'await db?.["$executeRawUnsafe"]?.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket call operation executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-call-operation-delete.ts":
|
||
'await db.$executeRawUnsafe["call"](db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed call operation executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-call-operation-delete.ts":
|
||
'await db.$executeRawUnsafe["ca" + "ll"](db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket bind executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-bind-delete.ts":
|
||
'await db["$executeRawUnsafe"].bind(db)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket bind operation executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-bind-operation-delete.ts":
|
||
'await db.$executeRawUnsafe["bind"](db)(\'DELETE FROM "AuditLog" WHERE id = $1\', id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed bind operation executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-bind-operation-delete.ts":
|
||
'await db.$executeRawUnsafe["bi" + "nd"](db)(\'DELETE FROM "AuditLog" WHERE id = $1\', id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through apply executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-apply-delete.ts":
|
||
'await db.$executeRawUnsafe.apply(db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through bracket apply operation executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-bracket-apply-operation-status.ts":
|
||
'await db.$executeRawUnsafe["apply"](db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-delete.ts":
|
||
'Function.prototype.call.call(db.$executeRawUnsafe, db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call.apply executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-apply-delete.ts":
|
||
'Function.prototype.call.apply(db.$executeRawUnsafe, [db, \'DELETE FROM "AuditLog" WHERE id = $1\', id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-alias-delete.ts": `
|
||
const c = Function.prototype.call;
|
||
c.call(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call alias apply executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-alias-apply-delete.ts": `
|
||
const c = Function.prototype.call;
|
||
c.apply(db.$executeRawUnsafe, [db, 'DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through assigned Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-assigned-alias-delete.ts": `
|
||
let c;
|
||
c = Function.prototype.call;
|
||
c.call(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-status.ts":
|
||
'Reflect.apply(db.$executeRawUnsafe, db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply.call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-call-delete.ts": `
|
||
Reflect.apply.call(
|
||
Reflect,
|
||
db.$executeRawUnsafe,
|
||
db,
|
||
["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-apply-status.ts": `
|
||
Reflect.apply.apply(Reflect, [
|
||
db.$executeRawUnsafe,
|
||
db,
|
||
["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]
|
||
]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply Function.prototype.call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-function-prototype-call-delete.ts":
|
||
'Reflect.apply(Function.prototype.call, db.$executeRawUnsafe, [db, \'DELETE FROM "AuditLog" WHERE id = $1\', id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply alias Function.prototype.call executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-alias-function-prototype-call-delete.ts": `
|
||
const ra = Reflect.apply;
|
||
ra(Function.prototype.call, db.$executeRawUnsafe, [db, 'DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Function.prototype.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-apply-status.ts":
|
||
'Function.prototype.apply.call(db.$executeRawUnsafe, db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Function.prototype.apply.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-apply-apply-status.ts":
|
||
'Function.prototype.apply.apply(db.$executeRawUnsafe, [db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Function.prototype.apply alias executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-apply-alias-status.ts": `
|
||
const a = Function.prototype.apply;
|
||
a.call(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Function.prototype.apply alias apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-apply-alias-apply-status.ts": `
|
||
const a = Function.prototype.apply;
|
||
a.apply(db.$executeRawUnsafe, [db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply Function.prototype.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-function-prototype-apply-status.ts":
|
||
'Reflect.apply(Function.prototype.apply, db.$executeRawUnsafe, [db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through assigned Reflect.apply alias Function.prototype.apply executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-assigned-alias-function-prototype-apply-status.ts": `
|
||
let ra;
|
||
ra = Reflect.apply;
|
||
ra(Function.prototype.apply, db.$executeRawUnsafe, [db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-alias-direct-delete.ts": `
|
||
const ra = Reflect.apply;
|
||
ra(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through propagated Reflect.apply alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-propagated-alias-direct-delete.ts": `
|
||
const r1 = Reflect.apply;
|
||
const r2 = r1;
|
||
r2(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional Reflect.apply alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-reflect-apply-alias-direct-delete.ts": `
|
||
const ra = Reflect.apply;
|
||
ra?.(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bound Reflect.apply alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bound-reflect-apply-alias-delete.ts": `
|
||
const ra = Reflect.apply.bind(Reflect);
|
||
ra(db.$executeRawUnsafe, db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through null-bound Reflect.apply alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-null-bound-reflect-apply-alias-delete.ts": `
|
||
const ra = Reflect.apply.bind(null);
|
||
ra(db.$executeRawUnsafe, db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through optional bound Reflect.apply alias executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-optional-bound-reflect-apply-alias-status.ts": `
|
||
const ra = Reflect.apply.bind(Reflect);
|
||
ra?.(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bound Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bound-function-prototype-call-alias-delete.ts": `
|
||
const c = Function.prototype.call.bind(Function.prototype.call);
|
||
c(db.$executeRawUnsafe, db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket bound Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-bound-function-prototype-call-alias-delete.ts": `
|
||
const c = Function.prototype["call"].bind(Function.prototype["call"]);
|
||
c(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional bracket bound Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-bound-function-prototype-call-alias-delete.ts": `
|
||
const pc = Function.prototype["call"].bind(Function.prototype["call"]);
|
||
pc?.(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed bound Function.prototype.call alias executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-bound-function-prototype-call-alias-delete.ts": `
|
||
const c = Function.prototype["ca" + "ll"].bind(Function.prototype["ca" + "ll"]);
|
||
c(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through bracket bound Function.prototype.apply alias executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-bracket-bound-function-prototype-apply-alias-status.ts": `
|
||
const a = Function.prototype["apply"].bind(Function.prototype["apply"]);
|
||
a(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call bound target immediate call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-delete.ts": `
|
||
Function.prototype.call.bind(db.$executeRawUnsafe)(
|
||
db,
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional Function.prototype call bind target immediate call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-optional-call-bind-target-delete.ts": `
|
||
Function.prototype.call?.bind(db.$executeRawUnsafe)(
|
||
db,
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket Function.prototype call bind target immediate call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-bracket-call-bind-target-delete.ts": `
|
||
Function.prototype["call"].bind(db.$executeRawUnsafe)(
|
||
db,
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed Function.prototype call bind target immediate call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-computed-call-bind-target-delete.ts": `
|
||
Function.prototype["ca" + "ll"].bind(db.$executeRawUnsafe)(
|
||
db,
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Function.prototype.apply bound target immediate call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-apply-bind-target-status.ts": `
|
||
Function.prototype.apply.bind(db.$executeRawUnsafe)(
|
||
db,
|
||
['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through computed Function.prototype apply bind target immediate call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-function-prototype-computed-apply-bind-target-status.ts": `
|
||
Function.prototype["ap" + "ply"].bind(db.$executeRawUnsafe)(
|
||
db,
|
||
['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Function.prototype.call bound target alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-alias-delete.ts": `
|
||
const exec = Function.prototype.call.bind(db.$executeRawUnsafe);
|
||
exec(db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional Function.prototype.call bound target alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-function-prototype-call-bind-target-alias-delete.ts": `
|
||
const exec = Function.prototype.call.bind(db.$executeRawUnsafe);
|
||
exec?.(db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through assigned Function.prototype.call bound target alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-assigned-alias-delete.ts": `
|
||
let exec;
|
||
exec = Function.prototype.call.bind(db.$executeRawUnsafe);
|
||
exec(db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply computed executor access",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-computed-status.ts":
|
||
'Reflect.apply((db["$execute" + "Raw" + "Unsafe"]), db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect bracket apply executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-bracket-apply-delete.ts": `
|
||
Reflect["apply"](db.$executeRawUnsafe, db, [
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect computed apply executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-computed-apply-delete.ts": `
|
||
Reflect["ap" + "ply"](db.$executeRawUnsafe, db, [
|
||
'DELETE FROM "AuditLog" WHERE id = $1',
|
||
id
|
||
]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply executor name alias",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-alias-status.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
Reflect.apply(db[rawName], db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply executor name alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-alias-delete.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
Reflect.apply(db[rawName], db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL update through Reflect.apply executor name alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-reflect-apply-alias-update.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
Reflect.apply(db[rawName], db, ["UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "dynamic raw SQL write through Reflect.apply executor name alias",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-alias-dynamic.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
const tableName = process.env.TABLE;
|
||
const query = "UPDATE " + tableName + " SET status = $1";
|
||
Reflect.apply(db[rawName], db, [query, status]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply plain executor alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-plain-alias-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
Reflect.apply(execRaw, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through Reflect.apply bound executor alias",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-bound-alias-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
Reflect.apply(execRaw, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL update through Reflect.apply assigned executor alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-reflect-apply-assigned-alias-update.ts": `
|
||
let execRaw;
|
||
execRaw = db.$executeRawUnsafe;
|
||
Reflect.apply(execRaw, db, ['UPDATE "GameVersion" SET "status" = $1 WHERE id = $2', status, id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "dynamic raw SQL write through Reflect.apply destructured executor alias",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-reflect-apply-destructured-alias-dynamic.ts": `
|
||
const { $executeRawUnsafe: execRaw } = db;
|
||
const tableName = process.env.TABLE;
|
||
const query = "UPDATE " + tableName + " SET status = $1";
|
||
Reflect.apply(execRaw, db, [query, status]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Reflect.apply computed destructured executor alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-reflect-apply-computed-destructured-alias-delete.ts": `
|
||
const rawName = "$executeRawUnsafe";
|
||
const { [rawName]: execRaw } = db;
|
||
Reflect.apply(execRaw, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through assigned executor name alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-assigned-computed-key-delete.ts": `
|
||
let rawName;
|
||
rawName = "$executeRawUnsafe";
|
||
await db[rawName]('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional bracket bind executor access",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-bind-delete.ts":
|
||
'await db?.["$executeRawUnsafe"]?.bind(db)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through bracket bound alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-bound-alias-delete.ts": `
|
||
const execRaw = db["$executeRawUnsafe"].bind(db);
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through plain alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-plain-alias-direct-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through mixed ternary bound executor alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-mixed-ternary-bound-alias-delete.ts": `
|
||
const execRaw = ok ? db.$executeRawUnsafe.bind(db) : safe;
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through propagated plain alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-propagated-plain-alias-direct-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
const runRaw = execRaw;
|
||
await runRaw('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through parenthesized plain alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-parenthesized-plain-alias-direct-delete.ts": `
|
||
const execRaw = (db.$executeRawUnsafe);
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through destructured executor alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-destructured-alias-direct-delete.ts": `
|
||
const { $executeRawUnsafe } = db;
|
||
await $executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through renamed destructured executor alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-renamed-destructured-alias-direct-delete.ts": `
|
||
const { $executeRawUnsafe: execRaw } = db;
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through quoted destructured executor alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-quoted-destructured-alias-direct-delete.ts": `
|
||
const { "$executeRawUnsafe": execRaw } = db;
|
||
await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through computed destructured executor alias direct call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-computed-destructured-alias-direct-delete.ts": `
|
||
const { ["$executeRawUnsafe"]: execRaw } = db;
|
||
await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through post-declaration destructured executor alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-assigned-destructured-alias-delete.ts": `
|
||
let execRaw;
|
||
({ $executeRawUnsafe: execRaw } = db);
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through assigned computed destructured executor alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-assigned-computed-destructured-alias-delete.ts": `
|
||
let execRaw;
|
||
({ ["$executeRawUnsafe"]: execRaw } = db);
|
||
await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through assigned executor alias direct call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-assigned-alias-direct-status.ts": `
|
||
let execRaw;
|
||
execRaw = db.$executeRawUnsafe;
|
||
await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through assigned logical plain executor alias",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-assigned-logical-plain-alias-status.ts": `
|
||
let runRaw;
|
||
runRaw = maybe || db.$executeRawUnsafe;
|
||
await runRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through post-declaration destructured executor alias",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-assigned-destructured-alias-status.ts": `
|
||
let $executeRawUnsafe;
|
||
({ $executeRawUnsafe } = db);
|
||
await $executeRawUnsafe("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through assigned bound executor alias direct call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-assigned-bound-alias-direct-status.ts": `
|
||
let execRaw;
|
||
execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through propagated bound alias direct call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-propagated-bound-alias-direct-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
const runRaw = execRaw;
|
||
await runRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL Job status write through plain alias direct call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-plain-alias-optional-direct-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw?.("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through plain alias call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-plain-alias-call-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through plain alias call",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-plain-alias-call-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw.call(db, "UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL AuditLog delete through plain alias call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-plain-alias-optional-call-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw?.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through plain alias bind",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-plain-alias-bind-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw.bind(db)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through plain alias bind",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-plain-alias-bind-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL Job status write through plain alias bind",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-plain-alias-optional-bind-status.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw?.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional bracket plain alias call",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-plain-alias-call-delete.ts": `
|
||
const execRaw = db?.["$executeRawUnsafe"];
|
||
await execRaw.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "no-semicolon bracket raw SQL AuditLog delete through bound alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bracket-bound-alias-no-semicolon-delete.ts": `
|
||
const execRaw = db["$executeRawUnsafe"].bind(db)
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id)
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "no-semicolon optional bracket raw SQL AuditLog delete through bound alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-bracket-bound-alias-no-semicolon-delete.ts": `
|
||
const execRaw = db?.["$executeRawUnsafe"]?.bind(db)
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id)
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through object property wrapped executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-object-property-wrapper-delete.ts": `
|
||
const ops = { execRaw: db.$executeRawUnsafe };
|
||
await ops.execRaw.call(db, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through object wrapper destructured executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-object-wrapper-destructured-delete.ts": `
|
||
const ops = { execRaw: db.$executeRawUnsafe };
|
||
const { execRaw } = ops;
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through object wrapper property alias",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-object-wrapper-property-alias-delete.ts": `
|
||
const ops = { execRaw: db.$executeRawUnsafe };
|
||
const runRaw = ops.execRaw;
|
||
await runRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through object shorthand wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-object-shorthand-wrapper-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
const ops = { execRaw };
|
||
await ops.execRaw('DELETE FROM "AuditLog" WHERE id=1');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through Object.assign shorthand wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-object-assign-shorthand-wrapper-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
const box = Object.assign({}, { execRaw });
|
||
await box.execRaw('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through array wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-array-wrapper-delete.ts": `
|
||
const ops = [db.$executeRawUnsafe];
|
||
await ops[0]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through array wrapper skipped destructured executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-array-wrapper-skipped-destructured-delete.ts": `
|
||
const ops = [1, db.$executeRawUnsafe.bind(db)];
|
||
const [, execRaw] = ops;
|
||
await execRaw('DELETE FROM "AuditLog" WHERE id=1');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through array wrapper assigned skipped destructured executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-array-wrapper-assigned-skipped-destructured-delete.ts": `
|
||
const ops = [1, db.$executeRawUnsafe.bind(db)];
|
||
let execRaw;
|
||
([, execRaw] = ops);
|
||
await execRaw('DELETE FROM "AuditLog" WHERE id=1');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through function return wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-return-wrapper-delete.ts": `
|
||
function raw() { return db.$executeRawUnsafe; }
|
||
await raw()("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through function returned object wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-return-object-wrapper-delete.ts": `
|
||
async function f(db) {
|
||
function getBox() {
|
||
return { raw: db.$executeRawUnsafe.bind(db) };
|
||
}
|
||
const { raw } = getBox();
|
||
await raw('DELETE FROM "AuditLog" WHERE id=1');
|
||
}
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through parenthesized function returned object wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-parenthesized-function-return-object-wrapper-delete.ts": `
|
||
function rawBox() { return ({ execRaw: db.$executeRawUnsafe }); }
|
||
rawBox().execRaw('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through function returned array wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-return-array-wrapper-delete.ts": `
|
||
function getBox() { return [db.$executeRawUnsafe.bind(db)]; }
|
||
await getBox()[0]('DELETE FROM "AuditLog" WHERE id=1');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL guarded state truncate through parenthesized function returned array wrapper executor",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/review-raw-parenthesized-function-return-array-wrapper-truncate.ts": `
|
||
function rawArray() { return ([db.$executeRawUnsafe]); }
|
||
rawArray()[0]('TRUNCATE TABLE "ReviewRecord"');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through parenthesized arrow returned object wrapper executor",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-parenthesized-arrow-return-object-wrapper-status.ts": `
|
||
const rawArrowBox = () => ({ execRaw: db.$executeRawUnsafe });
|
||
rawArrowBox().execRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through typed arrow return wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-typed-arrow-return-wrapper-delete.ts": `
|
||
const raw = (): typeof db.$executeRawUnsafe => db.$executeRawUnsafe;
|
||
raw()('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job status write through typed arrow returned object wrapper executor",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-raw-typed-arrow-return-object-wrapper-status.ts": `
|
||
const box = (): { execRaw: typeof db.$executeRawUnsafe } => ({ execRaw: db.$executeRawUnsafe });
|
||
box().execRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through nested object wrapper destructured executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-nested-object-wrapper-destructured-delete.ts": `
|
||
const raw = { inner: { exec: db.$executeRawUnsafe } };
|
||
const { inner: { exec } } = raw;
|
||
await exec('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through arrow return wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-arrow-return-wrapper-delete.ts": `
|
||
const raw = () => db.$executeRawUnsafe;
|
||
await raw()('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through block arrow return wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-block-arrow-return-wrapper-delete.ts": `
|
||
const getRaw = () => { return db.$executeRawUnsafe.bind(db); };
|
||
await getRaw()('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through function expression return wrapper executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-function-expression-return-wrapper-delete.ts": `
|
||
const raw = function () { return db.$executeRawUnsafe; };
|
||
await raw()('DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through call outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-call-delete.ts":
|
||
'await db.$executeRawUnsafe.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL AuditLog delete through call outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-call-delete.ts":
|
||
'await db.$executeRawUnsafe?.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "optional raw SQL AuditLog delete outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-optional-delete.ts":
|
||
'await db.$executeRawUnsafe?.("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "bound raw SQL AuditLog delete alias outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-bound-alias-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog multi-table truncate outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-raw-multi-truncate.ts":
|
||
'await db.$executeRawUnsafe("TRUNCATE TABLE \\"Other\\", \\"AuditLog\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog IF EXISTS trigger disable outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-disable-trigger-if-exists.ts":
|
||
'await db.$executeRawUnsafe("ALTER TABLE IF EXISTS \\"AuditLog\\" DISABLE TRIGGER ALL");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog trigger disable outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-disable-trigger.ts":
|
||
'await db.$executeRawUnsafe("ALTER TABLE \\"AuditLog\\" DISABLE TRIGGER \\"AuditLog_reject_delete\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog drop trigger outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-trigger.ts":
|
||
'await db.$executeRawUnsafe("DROP TRIGGER audit_append_only ON \\"AuditLog\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog quoted drop trigger outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-trigger-quoted.ts":
|
||
'await db.$executeRawUnsafe("DROP TRIGGER \\"audit-append-only\\" ON \\"AuditLog\\";");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog multi-table drop outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-table-list.ts":
|
||
'await db.$executeRawUnsafe("DROP TABLE \\"Other\\", \\"AuditLog\\"");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog alter table drop outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-table-drop.ts":
|
||
'await db.$executeRawUnsafe("ALTER TABLE \\"AuditLog\\" DROP CONSTRAINT foo");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog drop append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-function.ts":
|
||
'await db.$executeRawUnsafe("DROP FUNCTION app_reject_audit_log_mutation() CASCADE");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog unicode-codepoint drop append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-function-unicode-codepoint.ts":
|
||
'await db.$executeRawUnsafe("DROP FUNCTION \\u{61}pp_reject_audit_log_mutation() CASCADE");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog replace append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-replace-function.ts":
|
||
'await db.$executeRawUnsafe("CREATE OR REPLACE FUNCTION app_reject_audit_log_mutation() RETURNS trigger AS $$ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog alter append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-function.ts":
|
||
'await db.$executeRawUnsafe("ALTER FUNCTION app_reject_audit_log_mutation() OWNER TO app");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog alter IF EXISTS append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-function-if-exists.ts":
|
||
'await db.$executeRawUnsafe("ALTER FUNCTION IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog multi-function drop append-only function outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-function-list.ts":
|
||
'await db.$executeRawUnsafe("DROP FUNCTION IF EXISTS harmless(), app_reject_audit_log_mutation() CASCADE");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog drop append-only routine outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-routine.ts":
|
||
'await db.$executeRawUnsafe("DROP ROUTINE IF EXISTS app_reject_audit_log_mutation() CASCADE");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog alter append-only routine outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-routine.ts":
|
||
'await db.$executeRawUnsafe("ALTER ROUTINE app_reject_audit_log_mutation() OWNER TO app");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog alter IF EXISTS append-only routine outside trigger assertion",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-routine-if-exists.ts":
|
||
'await db.$executeRawUnsafe("ALTER ROUTINE IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres");'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic drop function outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-dynamic-function.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "DROP FUNCTION " + fn + " CASCADE";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic alter function outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-dynamic-function.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "ALTER FUNCTION " + fn + " OWNER TO app";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic drop routine outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-dynamic-routine.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "DROP ROUTINE " + fn + " CASCADE";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic alter routine outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-dynamic-routine.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "ALTER ROUTINE " + fn + " OWNER TO app";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic drop function list outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-dynamic-function-list.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "DROP FUNCTION IF EXISTS harmless(), " + fn + " CASCADE";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic create function outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-create-dynamic-function.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "CREATE FUNCTION " + fn + " RETURNS trigger AS $$ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL dynamic create or replace function outside trigger assertion",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-replace-dynamic-function.ts": `
|
||
const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()';
|
||
const query = "CREATE OR REPLACE FUNCTION " + fn + " RETURNS trigger AS $$ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration replace AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010102_replace_audit_guard/migration.sql": `
|
||
CREATE OR REPLACE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration block-comment tampered AuditLog append-only function with RETURN NEW",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010108_block_comment_tampered_audit_guard_return/migration.sql": `
|
||
CREATE/*x*/FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RETURN NEW;
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration tampered AuditLog append-only function with RETURN NEW",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010105_tampered_audit_guard_return/migration.sql": `
|
||
CREATE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RETURN NEW;
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration tampered AuditLog append-only function with IF branch",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010106_tampered_audit_guard_if/migration.sql": `
|
||
CREATE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
IF false THEN
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END IF;
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration mixed canonical and bad AuditLog append-only function definitions",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010107_mixed_audit_guard/migration.sql": `
|
||
CREATE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE FUNCTION app_reject_audit_log_mutation()
|
||
RETURNS trigger AS $$
|
||
BEGIN
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "migration drop AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010103_drop_audit_guard/migration.sql":
|
||
"DROP FUNCTION app_reject_audit_log_mutation() CASCADE;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration drop AuditLog append-only function in list",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010120_drop_audit_guard_function_list/migration.sql":
|
||
"DROP FUNCTION IF EXISTS harmless(), app_reject_audit_log_mutation() CASCADE;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration drop AuditLog append-only routine",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010121_drop_audit_guard_routine/migration.sql":
|
||
"DROP ROUTINE IF EXISTS app_reject_audit_log_mutation() CASCADE;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration block-comment drop AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010109_block_comment_drop_audit_guard/migration.sql":
|
||
"DROP/*x*/FUNCTION app_reject_audit_log_mutation() CASCADE;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration nested block-comment drop AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010116_nested_block_comment_drop_audit_guard/migration.sql":
|
||
"DROP/* outer /* inner */ still outer */FUNCTION app_reject_audit_log_mutation() CASCADE;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration alter AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010104_alter_audit_guard/migration.sql":
|
||
"ALTER FUNCTION app_reject_audit_log_mutation() OWNER TO app;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration alter IF EXISTS AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010124_alter_if_exists_audit_guard/migration.sql":
|
||
"ALTER FUNCTION IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration alter AuditLog append-only routine",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010122_alter_audit_guard_routine/migration.sql":
|
||
"ALTER ROUTINE app_reject_audit_log_mutation() OWNER TO app;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration alter IF EXISTS AuditLog append-only routine",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010125_alter_if_exists_audit_guard_routine/migration.sql":
|
||
"ALTER ROUTINE IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration block-comment alter AuditLog append-only function",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010110_block_comment_alter_audit_guard/migration.sql":
|
||
"ALTER/*x*/FUNCTION app_reject_audit_log_mutation() OWNER TO app;"
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog block-comment delete",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010111_block_comment_audit_delete/migration.sql":
|
||
'DELETE/*x*/FROM "AuditLog" WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog unicode quoted identifier delete",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010119_unicode_quoted_audit_delete/migration.sql":
|
||
'DELETE FROM U&"Audit\\004Cog" WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog custom UESCAPE unicode quoted identifier delete",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010123_unicode_quoted_custom_uescape_audit_delete/migration.sql":
|
||
'DELETE FROM U&"Audit!004Cog" UESCAPE \'!\' WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog inheritance update",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010118_audit_inheritance_update/migration.sql":
|
||
'UPDATE "AuditLog" * SET "action" = \'changed\' WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog nested block-comment delete",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010117_nested_block_comment_audit_delete/migration.sql":
|
||
'DELETE/* outer /* inner */ still outer */FROM "AuditLog" WHERE id = \'x\';'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog delete after line-comment-looking string literal",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010115_audit_delete_after_string_line_comment/migration.sql":
|
||
`SELECT '-- not a comment'; DELETE FROM "AuditLog" WHERE id = 'x';`
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog upsert",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010112_audit_upsert/migration.sql":
|
||
'INSERT INTO "AuditLog" ("id", "action") VALUES (\'x\', \'created\') ON CONFLICT ("id") DO UPDATE SET "action" = EXCLUDED."action";'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog merge",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010113_audit_merge/migration.sql":
|
||
'MERGE INTO "AuditLog" a USING "AuditLogPatch" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET "action" = p."action";'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL AuditLog block-comment merge",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010114_block_comment_audit_merge/migration.sql":
|
||
'MERGE/*x*/INTO "AuditLog" a USING "AuditLogPatch" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET "action" = p."action";'
|
||
}
|
||
},
|
||
{
|
||
label: "migration SQL Job block-comment merge outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/prisma/migrations/20260602010112_block_comment_job_merge/migration.sql":
|
||
'MERGE/*x*/INTO "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through direct generic executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-direct-generic-raw-delete.ts":
|
||
'await db.$executeRawUnsafe<number>("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job update through direct generic executor",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-direct-generic-raw-update.ts":
|
||
'await db.$executeRaw<number>("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL Job update through bound alias generic executor",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bound-alias-generic-raw-update.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw<number>("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through plain alias generic executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-plain-alias-generic-raw-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw<number>("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through optional plain alias generic executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-optional-plain-alias-generic-raw-delete.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw?.<number>("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog delete through wrapped generic executor",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-wrapped-generic-raw-delete.ts": `
|
||
const box = { execRaw: db.$executeRawUnsafe.bind(db) };
|
||
await box.execRaw<number>("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog trigger disable through intermediate table constant",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-disable-trigger-intermediate.ts": `
|
||
const tableName = '"AuditLog"';
|
||
const query = "ALTER TABLE " + tableName + " DISABLE TRIGGER ALL";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog IF EXISTS trigger disable through dynamic table variable",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-disable-trigger-if-exists-dynamic-table.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "ALTER TABLE IF EXISTS " + tableName + " DISABLE TRIGGER ALL";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL multi-table truncate through dynamic table variable",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-multi-truncate-dynamic-table.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "TRUNCATE TABLE \\"Other\\", " + tableName;
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL AuditLog trigger disable through dynamic table variable",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-disable-trigger-dynamic-table.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "ALTER TABLE " + tableName + " DISABLE TRIGGER ALL";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL drop trigger through dynamic trigger and table variables",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-trigger-dynamic.ts": `
|
||
const triggerName = process.env.TRIGGER_NAME ?? 'audit_append_only';
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "DROP TRIGGER " + triggerName + " ON " + tableName;
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL drop table through dynamic table variable",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-drop-table-dynamic.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "DROP TABLE " + tableName;
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "raw SQL alter table drop through dynamic table variable",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-alter-drop-dynamic.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = "ALTER TABLE " + tableName + " DROP CONSTRAINT foo";
|
||
await db.$executeRawUnsafe(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql AuditLog delete through Prisma.raw intermediate table constant",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-prisma-raw-intermediate.ts": `
|
||
const tableName = '"AuditLog"';
|
||
const query = Prisma.sql\`DELETE FROM \${Prisma.raw(tableName)} WHERE id = \${id}\`;
|
||
await db.$executeRaw(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql AuditLog delete through unicode-escaped Prisma.raw intermediate table constant",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-prisma-raw-unicode-intermediate.ts": `
|
||
const tableName = '"\\u0041uditLog"';
|
||
const query = Prisma.sql\`DELETE FROM \${Prisma.raw(tableName)} WHERE id = \${id}\`;
|
||
await db.$executeRaw(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Prisma.sql AuditLog delete through dynamic Prisma.raw table",
|
||
reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-prisma-raw-dynamic-table.ts": `
|
||
const tableName = process.env.AUDIT_TABLE ?? '"AuditLog"';
|
||
const query = Prisma.sql\`DELETE FROM \${Prisma.raw(tableName)} WHERE id = \${id}\`;
|
||
await db.$executeRaw(query);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write inside jobs module outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/jobs/bad.ts":
|
||
'await db.job.update({ where: { id }, data: { status: "failed", attempts: { increment: 1 }, nextRetryAt: null } });'
|
||
}
|
||
},
|
||
{
|
||
label: "direct Job status write inside jobs index outside JobExecutionStore class",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/jobs/index.ts": `
|
||
export class JobExecutionStore {
|
||
async ok(db) {
|
||
await db.job.update({ where: { id: "ok" }, data: { status: "running" } });
|
||
}
|
||
}
|
||
|
||
export class JobApiService {
|
||
async bad(db) {
|
||
await db.job.update({ where: { id: "bad" }, data: { status: "failed" } });
|
||
}
|
||
}
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "computed Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-computed-bracket-model.ts":
|
||
'await db["j" + "o" + "b"].update({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "parenthesized computed Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-parenthesized-computed-bracket-model.ts":
|
||
'await db[("j" + "o" + "b")].update({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "computed Job update method write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-computed-bracket-method.ts":
|
||
'await db.job[("up" + "date")]({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.call Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-function-prototype-call-status.ts":
|
||
"Function.prototype.call.call(db.job.update, db.job, { where: { id }, data: { status } });"
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.call propagated alias Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-function-prototype-call-propagated-alias-status.ts": `
|
||
const c1 = Function.prototype.call;
|
||
const c2 = c1;
|
||
c2.call(db.job.update, db.job, { where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "bracket call operation audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-bracket-call-operation-delete.ts":
|
||
'await db.auditLog.delete["call"](db.auditLog, { where: { id } });'
|
||
}
|
||
},
|
||
{
|
||
label: "bracket call operation guarded Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-bracket-call-operation-status.ts":
|
||
'await db["gameVersion"].update["call"](db.gameVersion, { where: { id }, data: { status: "x" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "bracket apply operation Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-apply-operation-status.ts":
|
||
'await db.job.update["apply"](db.job, [{ where: { id }, data: { status: "failed" } }]);'
|
||
}
|
||
},
|
||
{
|
||
label: "computed apply operation Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-computed-apply-operation-status.ts":
|
||
'await db.job.update["app" + "ly"](db.job, [{ where: { id }, data: { status: "failed" } }]);'
|
||
}
|
||
},
|
||
{
|
||
label: "bracket bind operation Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-bind-operation-status.ts":
|
||
'await db.job.update["bind"](db.job)({ where: { id }, data: { status: "failed" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "delegate propagated alias Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delegate-propagated-alias-status.ts": `
|
||
const jobs = db.job;
|
||
const queueJobs = jobs;
|
||
await queueJobs.update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate mixed ternary alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delegate-mixed-ternary-alias-delete.ts": `
|
||
const jobs = ok ? db.job : safeJobs;
|
||
await jobs.delete({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate assigned logical alias audit delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-delegate-assigned-logical-alias-delete.ts": `
|
||
let logs;
|
||
logs = maybe && db.auditLog;
|
||
await logs.delete({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate mixed ternary alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-mixed-ternary-alias-status.ts": `
|
||
const versions = ok ? db.gameVersion : safeVersions;
|
||
await versions.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate object wrapper destructured Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delegate-object-wrapper-destructured-status.ts": `
|
||
const ops = { jobs: db.job };
|
||
const { jobs } = ops;
|
||
await jobs.update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate nested object wrapper destructured guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-nested-object-wrapper-destructured-status.ts": `
|
||
const box = { inner: { version: db.gameVersion } };
|
||
const { inner: { version } } = box;
|
||
await version.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate array wrapper Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-delegate-array-wrapper-status.ts": `
|
||
const ops = [db.job];
|
||
await ops[0].update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate function return wrapper audit delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-delegate-function-return-wrapper-delete.ts": `
|
||
function audit() { return db.auditLog; }
|
||
await audit().delete({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate function returned object wrapper audit delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-delegate-function-return-object-wrapper-delete.ts": `
|
||
async function f(db) {
|
||
function getBox() {
|
||
return { logs: db.auditLog };
|
||
}
|
||
const { logs } = getBox();
|
||
await logs.delete({ where: { id: "1" } });
|
||
}
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate arrow return wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-arrow-return-wrapper-status.ts": `
|
||
const getVersion = () => db.gameVersion;
|
||
await getVersion().update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate block arrow return wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-block-arrow-return-wrapper-status.ts": `
|
||
const getVersion = () => { return db.gameVersion; };
|
||
await getVersion().update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate function returned object wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-function-return-object-wrapper-status.ts": `
|
||
async function f(db) {
|
||
function getBox() {
|
||
return { versions: db.gameVersion };
|
||
}
|
||
const { versions } = getBox();
|
||
await versions.update({ where: { id: "1" }, data: { status: "x" } });
|
||
}
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate parenthesized function returned object wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-delegate-parenthesized-function-return-object-wrapper-status.ts": `
|
||
function delegateBox() { return ({ versions: db.gameVersion }); }
|
||
delegateBox().versions.update({ where: { id }, data: { status: 'active' } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "delegate function expression return wrapper audit delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-delegate-function-expression-return-wrapper-delete.ts": `
|
||
const audit = function () { return db.auditLog; };
|
||
await audit().delete({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-object-wrapper-status.ts": `
|
||
const ops = { updateJob: db.job.update };
|
||
await ops.updateJob.call(db.job, { where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method bound alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-bound-alias-delete.ts": `
|
||
const deleteJob = db.job.delete.bind(db.job);
|
||
await deleteJob({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method plain alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-plain-alias-delete.ts": `
|
||
const del = db.job.delete;
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned plain alias Job deleteMany outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-assigned-plain-alias-delete-many.ts": `
|
||
let delMany;
|
||
delMany = db.job.deleteMany;
|
||
await delMany({ where: { status: "running" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method mixed ternary plain alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-mixed-ternary-plain-alias-delete.ts": `
|
||
const del = ok ? db.job.delete : safe;
|
||
await del.call(db.job, { where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method mixed ternary bound alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-mixed-ternary-bound-alias-delete.ts": `
|
||
const del = ok ? db.job.delete.bind(db.job) : safe;
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned bound alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-assigned-bound-alias-delete.ts": `
|
||
let del;
|
||
del = db.job.delete.bind(db.job);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method mixed assigned ternary bound alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-mixed-assigned-ternary-bound-alias-delete.ts": `
|
||
let del;
|
||
del = ok ? db.job.delete.bind(db.job) : safe;
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method destructured alias Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-destructured-alias-delete.ts": `
|
||
const { delete: deleteJob } = db.job;
|
||
await deleteJob.call(db.job, { where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method Object.assign wrapper Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-object-assign-wrapper-delete.ts": `
|
||
const deleteJob = db.job.delete.bind(db.job);
|
||
const box = Object.assign({}, { deleteJob });
|
||
await box.deleteJob({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method function return wrapper Job deleteMany outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-method-function-return-wrapper-delete-many.ts": `
|
||
const getDeleteMany = () => { return db.job.deleteMany.bind(db.job); };
|
||
await getDeleteMany()({ where: { status: "running" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method direct bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-direct-bound-alias-delete.ts": `
|
||
const del = db.auditLog.delete.bind(db.auditLog);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method plain alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-plain-alias-delete.ts": `
|
||
const deleteAudit = db.auditLog.delete;
|
||
await deleteAudit({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned mixed ternary plain alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-assigned-mixed-ternary-plain-alias-delete.ts": `
|
||
let del;
|
||
del = ok ? db.auditLog.delete : safe;
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-assigned-bound-alias-delete.ts": `
|
||
let del;
|
||
del = db.auditLog.delete.bind(db.auditLog);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method mixed ternary bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-mixed-ternary-bound-alias-delete.ts": `
|
||
const del = ok ? db.auditLog.delete.bind(db.auditLog) : safe;
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method destructured bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-destructured-bound-alias-delete.ts": `
|
||
const { delete: del0 } = db.auditLog;
|
||
const del = del0.bind(db.auditLog);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method direct bound alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-direct-bound-alias-status.ts": `
|
||
const updateVersion = db.gameVersion.update.bind(db.gameVersion);
|
||
await updateVersion({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned plain alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-assigned-plain-alias-status.ts": `
|
||
let updateVersion;
|
||
updateVersion = db.gameVersion.update;
|
||
await updateVersion({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method plain alias call guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-plain-alias-call-status.ts": `
|
||
const updateVersion = db.gameVersion.update;
|
||
await updateVersion.call(db.gameVersion, { where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method logical plain alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-logical-plain-alias-call-status.ts": `
|
||
const update = maybe || db.gameVersion.update;
|
||
await update.call(db.gameVersion, { where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method assigned bound alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-assigned-bound-alias-status.ts": `
|
||
let update;
|
||
update = db.gameVersion.update.bind(db.gameVersion);
|
||
await update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method mixed ternary bound alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-mixed-ternary-bound-alias-status.ts": `
|
||
const update = ok ? db.gameVersion.update.bind(db.gameVersion) : safe;
|
||
await update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method ternary bound alias guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-ternary-bound-alias-status.ts": `
|
||
const update = ok ? db.gameVersion.update.bind(db.gameVersion) : db.gameVersion.update.bind(db.gameVersion);
|
||
await update({ where: { id }, data: { status: 'active' } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method logical OR bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-logical-or-bound-alias-delete.ts": `
|
||
const del = maybe || db.auditLog.delete.bind(db.auditLog);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method logical AND bound alias audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-method-logical-and-bound-alias-delete.ts": `
|
||
const del = ok && db.auditLog.delete.bind(db.auditLog);
|
||
await del({ where: { id } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method Object.assign wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-assign-wrapper-status.ts": `
|
||
const update = db.gameVersion.update.bind(db.gameVersion);
|
||
const box = Object.assign({ safe: 1 }, { update });
|
||
await box.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method block arrow return wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-block-arrow-return-wrapper-status.ts": `
|
||
const getUpdate = () => { return db.gameVersion.update; };
|
||
await getUpdate().call(db.gameVersion, { where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper destructured guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-wrapper-destructured-status.ts": `
|
||
const ops = { update: db.gameVersion.update };
|
||
const { update } = ops;
|
||
await update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper assigned destructured guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-wrapper-assigned-destructured-status.ts": `
|
||
const ops = { update: db.gameVersion.update };
|
||
let update;
|
||
({ update } = ops);
|
||
await update.call(db.gameVersion, { where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper property alias guarded call outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-wrapper-property-alias-call-status.ts": `
|
||
const ops = { update: db.gameVersion.update };
|
||
const fn = ops.update;
|
||
await fn.call(db.gameVersion, { where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper property alias guarded apply outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-wrapper-property-alias-apply-status.ts": `
|
||
const ops = { update: db.gameVersion.update };
|
||
const fn = ops.update;
|
||
await fn.apply(db.gameVersion, [{ where: { id }, data: { status: "active" } }]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method object wrapper property alias guarded bind outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-object-wrapper-property-alias-bind-status.ts": `
|
||
const ops = { update: db.gameVersion.update };
|
||
const fn = ops.update;
|
||
await fn.bind(db.gameVersion)({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method function returned object wrapper property alias guarded call outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-function-return-object-wrapper-property-alias-call-status.ts": `
|
||
function getBox() { return { update: db.gameVersion.update }; }
|
||
const update = getBox().update;
|
||
await update.call(db.gameVersion, { where: { id }, data: { status: "x" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "method parenthesized function returned object wrapper guarded write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-method-parenthesized-function-return-object-wrapper-status.ts": `
|
||
function methodBox() { return ({ update: db.gameVersion.update }); }
|
||
methodBox().update({ where: { id }, data: { status: 'active' } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.call.apply Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-function-prototype-call-apply-status.ts":
|
||
"Function.prototype.call.apply(db.job.update, [db.job, { where: { id }, data: { status: 'failed' } }]);"
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.call.apply audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-function-prototype-call-apply-delete.ts":
|
||
"Function.prototype.call.apply(db.auditLog.delete, [db.auditLog, { where: { id } }]);"
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.call bound target audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-function-prototype-call-bind-target-delete.ts": `
|
||
Function.prototype.call.bind(db.auditLog.delete)(
|
||
db.auditLog,
|
||
{ where: { id } }
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype.apply bound target Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-function-prototype-apply-bind-target-status.ts": `
|
||
Function.prototype.apply.bind(db.job.update)(
|
||
db.job,
|
||
[{ where: { id }, data: { status: "failed" } }]
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Function.prototype computed apply bound target Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-function-prototype-computed-apply-bind-target-status.ts": `
|
||
Function.prototype["ap" + "ply"].bind(db.job.update)(
|
||
db.job,
|
||
[{ where: { id }, data: { status: "failed" } }]
|
||
);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "bracket bound Function.prototype.call alias Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-bracket-bound-function-prototype-call-alias-status.ts": `
|
||
const pc = Function.prototype["call"].bind(Function.prototype["call"]);
|
||
pc(db.job.update, db.job, { where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "optional bracket bound Function.prototype.call alias Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-optional-bracket-bound-function-prototype-call-alias-status.ts": `
|
||
const pc = Function.prototype["call"].bind(Function.prototype["call"]);
|
||
pc?.(db.job.update, db.job, { where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Reflect bracket apply guarded Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-reflect-bracket-apply-prisma-update.ts": `
|
||
Reflect["apply"](db.gameVersion.update, db.gameVersion, [
|
||
{ where: { id }, data: { status: "active" } }
|
||
]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "null-bound Reflect.apply Job delete outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-null-bound-reflect-apply-delete.ts": `
|
||
const apply = Reflect.apply.bind(null);
|
||
apply(db.job.delete, db.job, [{ where: { id } }]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "null-bound Reflect.apply audit delegate delete outside audit module",
|
||
reason: "AUDIT_UPDATE_DELETE_FORBIDDEN",
|
||
files: {
|
||
"apps/api/src/modules/projects/audit-null-bound-reflect-apply-delete.ts": `
|
||
const apply = Reflect.apply.bind(null);
|
||
apply(db.auditLog.delete, db.auditLog, [{ where: { id } }]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "null-bound Reflect.apply guarded Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-null-bound-reflect-apply-prisma-update.ts": `
|
||
const apply = Reflect.apply.bind(null);
|
||
apply(db.gameVersion.update, db.gameVersion, [{ where: { id }, data: { status: "active" } }]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "Reflect.apply.call Function.prototype.call Job status write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-reflect-apply-call-function-prototype-call-status.ts": `
|
||
Reflect.apply.call(Reflect, Function.prototype.call, db.job.update, [
|
||
db.job,
|
||
{ where: { id }, data: { status } }
|
||
]);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "computed destructured Job delegate alias write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-computed-destructured-alias.ts": `
|
||
const { ["j" + "o" + "b"]: jobs } = db;
|
||
await jobs.update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "assigned variable computed Job delegate write outside JobExecutionStore",
|
||
reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
files: {
|
||
"apps/api/src/modules/projects/job-assigned-computed-model.ts": `
|
||
let modelName;
|
||
modelName = "job";
|
||
await db[modelName].update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version.ts": "await db.gameVersion.update({ where: { id }, data: { status: 'active' } });"
|
||
}
|
||
},
|
||
{
|
||
label: "guarded optional Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-optional.ts":
|
||
'await db.gameVersion?.update({ where: { id }, data: { status: "active" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded parenthesized Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-parenthesized.ts":
|
||
'await (db.gameVersion).update({ where: { id }, data: { status: "active" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded delegate alias Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-alias.ts":
|
||
'const versions = db.gameVersion; await versions.update({ where: { id }, data: { status: "active" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded assigned delegate alias Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-assigned-alias.ts": `
|
||
let versions;
|
||
versions = db.gameVersion;
|
||
await versions.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded assigned destructured delegate alias Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-assigned-destructured-alias.ts": `
|
||
let versions;
|
||
({ gameVersion: versions } = db);
|
||
await versions.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded assigned computed destructured delegate alias Prisma write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-assigned-computed-destructured-alias.ts": `
|
||
let versions;
|
||
({ ["game" + "Version"]: versions } = db);
|
||
await versions.update({ where: { id }, data: { status: "active" } });
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded Prisma bracket model write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-bracket-model.ts":
|
||
'await db["gameVersion"].update({ where: { id }, data: { status: "active" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded Prisma computed bracket model write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-computed-bracket-model.ts":
|
||
'await db["game" + "Version"].update({ where: { id }, data: { status: "active" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded Prisma bracket method write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/review-bracket-method.ts":
|
||
'await db.reviewRecord["update"]({ where: { id }, data: { status: "approved" } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded Prisma bracket model and method write outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/lifecycle-bracket-model-method.ts":
|
||
'await db["lifecycleEvent"]["delete"]({ where: { eventId } });'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL simple update outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-update.ts":
|
||
'await db.$executeRawUnsafe("UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded parenthesized raw SQL update outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-parenthesized-update.ts":
|
||
'await (db.$executeRawUnsafe)("UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL block-comment update through optional bracket outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-block-comment-update.ts":
|
||
'await db?.["$executeRawUnsafe"]?.("UPDATE/*x*/\\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL UESCAPE update through call outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-uescape-update.ts":
|
||
'await db.$executeRawUnsafe.call(db, "UPDATE U&\\"Game!0056ersion\\" UESCAPE \'!\' SET \\"status\\" = $1 WHERE id = $2", status, id);'
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL inheritance update through bound alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-bound-alias-update.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw('UPDATE ONLY "GameVersion" * gv SET "status" = $1 WHERE gv.id = $2', status, id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL insert through plain alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/review-raw-plain-alias-insert.ts": `
|
||
const execRaw = db.$executeRawUnsafe;
|
||
await execRaw('INSERT INTO "ReviewRecord" ("id") VALUES ($1)', id);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL delete through assigned plain alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/lifecycle-raw-assigned-plain-alias-delete.ts": `
|
||
let execRaw;
|
||
execRaw = db.$executeRawUnsafe;
|
||
await execRaw('DELETE FROM "LifecycleEvent" WHERE "eventId" = $1', eventId);
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL merge through destructured alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/version-raw-destructured-alias-merge.ts": `
|
||
const { $executeRawUnsafe: execRaw } = db;
|
||
await execRaw('MERGE INTO "GameVersion" gv USING "GameVersionPatch" p ON gv.id = p.id WHEN MATCHED THEN UPDATE SET "status" = p."status"');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "guarded raw SQL truncate through assigned bound alias outside state-transition",
|
||
reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION",
|
||
files: {
|
||
"apps/api/src/modules/projects/review-raw-assigned-bound-alias-truncate.ts": `
|
||
let execRaw;
|
||
execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw('TRUNCATE TABLE "ReviewRecord"');
|
||
`
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase creation-agent messages path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/creation-agent/messages/index.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase creation-agent messages single-file path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/creation-agent/messages.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase compile-game-ir module path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/compile-game-ir/index.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase events batch route path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/events/batch/index.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase events batch route content without leading slash",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/events/index.ts": 'export const route = "events/batch";'
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase events batch single-file path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/events/batch.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase PromptRecord app implementation content",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/projects/prompt-record.ts": "export interface PromptRecord { rawPrompt: string; }"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase DesignSection app implementation content",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/projects/design-section.ts": "export interface DesignSection { projectId: string; payloadJson: unknown; }"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase DesignSectionPatch app implementation content",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/projects/design-section-patch.ts": "export interface DesignSectionPatch { id: string; patch: unknown; }"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase AIGameDesignDraft filename path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/AIGameDesignDraft.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase GameIR app module path",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/projects/game-ir.ts": "export const harmless = true;"
|
||
}
|
||
},
|
||
{
|
||
label: "later-phase GameIR app implementation content",
|
||
reason: "LATER_PHASE_SCOPE_LEAK",
|
||
files: {
|
||
"apps/api/src/modules/projects/index.ts": "class GameIRService {}"
|
||
}
|
||
}
|
||
];
|
||
|
||
for (const fixture of failingFixtures) {
|
||
await withFixture(fixture.files, async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, fixture.reason, fixture.label);
|
||
});
|
||
}
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/prisma/schema.prisma": `
|
||
model MainCreationAgentSession { id String @id }
|
||
model AgentTask { id String @id }
|
||
model ReviewRecord { id String @id }
|
||
model LifecycleEvent { eventId String @id }
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "S1 anchor models");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/prisma.schema.spec.ts": `
|
||
const route = "creation-agent/messages";
|
||
await db.job.update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "historical prisma schema test later-phase leak");
|
||
assertHasReason(violations, "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", "historical prisma schema test Job write");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/state-transition/index.ts": 'import "./index.js"; export class StateTransitionService {}',
|
||
"apps/api/src/modules/state-transition/self.ts":
|
||
'await import(`../state-transition/index.js`); require(`../state-transition/index.js`); const r = require; r(`../state-transition/index.js`); let rr; rr = require; rr("../state-transition/index.js");',
|
||
"apps/api/src/modules/projects/index.ts":
|
||
'import "../state-transition/index.js"; await import("../state-transition/index.js"); require("../state-transition/index.js"); await import(`../state-transition/index.js`); require(`../state-transition/index.js`); await import("../state-transition" + "/index.js"); require("../state-" + "transition/index.js"); const mod = "../state-transition/index.js"; await import(mod); require(mod); const stateTransitionPath = "../state-transition/index.js"; const recursiveMod = stateTransitionPath; await import(recursiveMod); const base = "../state-transition"; const concatenatedMod = base + "/index.js"; require(concatenatedMod);'
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "allowed state-transition import paths");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/state-transition/raw-guarded.ts": `
|
||
const execRaw = db.$executeRawUnsafe.bind(db);
|
||
await execRaw('UPDATE "GameVersion" SET "status" = $1 WHERE id = $2', status, id);
|
||
await db.$executeRawUnsafe('TRUNCATE TABLE "ReviewRecord"');
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "allowed state-transition raw guarded write path");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/audit/audit.spec.ts": `
|
||
await expectPrismaRejectedInSavepoint(
|
||
tx,
|
||
() => tx.$executeRawUnsafe('UPDATE "AuditLog" SET "action" = $1 WHERE id = $2', action, id),
|
||
/AuditLog is append-only/
|
||
);
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "AuditLog append-only raw SQL trigger assertion");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/dynamic-select.ts": `
|
||
const tableName = process.env.READ_TABLE ?? '"Job"';
|
||
const query = "SELECT * FROM " + tableName + " WHERE id = $1";
|
||
await db.$queryRawUnsafe(query, id);
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "dynamic raw SQL SELECT");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/nonraw-wrapper.ts": [
|
||
"const nonRaw = (sql) => sql;",
|
||
"const f1 = nonRaw;",
|
||
"const f2 = f1;",
|
||
"f2('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"const ops = { execRaw: nonRaw };",
|
||
"const { execRaw } = ops;",
|
||
'execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1");',
|
||
"",
|
||
"const ops2 = { execRaw: nonRaw };",
|
||
"ops2.execRaw.call(null, 'DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"const shorthand = nonRaw;",
|
||
"const opsShorthand = { shorthand };",
|
||
"opsShorthand.shorthand('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const assignedBox = Object.assign({}, { shorthand });",
|
||
"assignedBox.shorthand('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"const arr = [nonRaw];",
|
||
"arr[0]('UPDATE \"Job\" SET \"status\" = $1');",
|
||
"const arr2 = [1, nonRaw];",
|
||
"const [, arrExec] = arr2;",
|
||
"arrExec('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"let assignedArrExec;",
|
||
"([, assignedArrExec] = arr2);",
|
||
"assignedArrExec('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"function raw() { return nonRaw; }",
|
||
"raw()('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"function getBox() { return { raw: nonRaw, nested: { exec: nonRaw }, arr: [nonRaw] }; }",
|
||
"const { raw: boxedRaw, nested: { exec: nestedBoxedRaw } } = getBox();",
|
||
"boxedRaw('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"nestedBoxedRaw('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const boxedAlias = getBox().raw;",
|
||
"boxedAlias('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"getBox().arr[0]('UPDATE \"Job\" SET \"status\" = $1');",
|
||
"",
|
||
"function getParenBox() { return ({ raw: nonRaw, arr: [nonRaw] }); }",
|
||
"getParenBox().raw('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"getParenBox().arr[0]('UPDATE \"Job\" SET \"status\" = $1');",
|
||
"",
|
||
"const nested = { inner: { exec: nonRaw } };",
|
||
"const { inner: { exec } } = nested;",
|
||
"exec('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"const rawArrow = () => nonRaw;",
|
||
"rawArrow()('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const blockArrow = () => { return nonRaw; };",
|
||
"blockArrow()('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const logicalNonRaw = maybe || nonRaw;",
|
||
"logicalNonRaw('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const ternaryNonRaw = ok ? nonRaw : nonRaw;",
|
||
"ternaryNonRaw('UPDATE \"Job\" SET \"status\" = $1');",
|
||
"const conditionalNonRaw = ok ? nonRaw : safe;",
|
||
"conditionalNonRaw('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const typedRaw = (): typeof nonRaw => nonRaw;",
|
||
"typedRaw()('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"const typedBox = (): { raw: typeof nonRaw } => ({ raw: nonRaw });",
|
||
"typedBox().raw('UPDATE \"Job\" SET \"status\" = $1');",
|
||
"",
|
||
"const rawExpression = function () { return nonRaw; };",
|
||
"rawExpression()('DELETE FROM \"AuditLog\" WHERE id = $1');",
|
||
"",
|
||
"const ordinary = { inner: { update: nonRaw } };",
|
||
"const { inner: { update } } = ordinary;",
|
||
'update({ where: { id }, data: { status: "active" } });',
|
||
"const deleteJob = (args) => args;",
|
||
"await deleteJob({ where: { id } });",
|
||
"const deleteBox = Object.assign({}, { deleteJob });",
|
||
"await deleteBox.deleteJob({ where: { id } });",
|
||
"const getDeleteMany = () => { return deleteJob; };",
|
||
'await getDeleteMany()({ where: { status: "running" } });',
|
||
"const safeJobs = { delete: (args) => args };",
|
||
"const otherJobs = { delete: (args) => args };",
|
||
"const jobs = ok ? safeJobs : otherJobs;",
|
||
"await jobs.delete({ where: { id } });",
|
||
"const safeDelete = (args) => args;",
|
||
"const otherSafeDelete = (args) => args;",
|
||
"const safeDel = ok ? safeDelete : otherSafeDelete;",
|
||
"await safeDel.call(db.job, { where: { id } });",
|
||
"await nonRaw<number>('DELETE FROM \"AuditLog\" WHERE id = $1', id);",
|
||
'await import(`../${"safe-module"}/index.js`);',
|
||
'const seg = "safe-module";',
|
||
"await import(`../${seg}/index.js`);"
|
||
].join("\n")
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "non raw wrapper aliases");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/job-cleanup.spec.ts": `
|
||
const runId = "scope-test";
|
||
await prisma.job.deleteMany({ where: { id: { startsWith: runId } } });
|
||
await db.job.deleteMany({ where: { id: { startsWith: runId } } });
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "test-only Job runId cleanup");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/dynamic-select-unicode-quoted.ts": `
|
||
const tableName = process.env.READ_TABLE ?? 'U&"J\\\\006Fb"';
|
||
const query = "SELECT * FROM " + tableName + " WHERE id = $1";
|
||
await db.$queryRawUnsafe(query, id);
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "dynamic raw SQL SELECT with unicode quoted identifier");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/projects/non-raw-reflect-apply.ts": `
|
||
const nonRaw = (sql) => sql;
|
||
Reflect.apply(nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']);
|
||
Function.prototype.call.call(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
const c = Function.prototype.call;
|
||
const ra = Reflect.apply;
|
||
c.call(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1', id);
|
||
ra(nonRaw, null, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]);
|
||
Reflect.apply.call(Reflect, nonRaw, null, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);
|
||
const boundReflectApply = Reflect.apply.bind(Reflect);
|
||
boundReflectApply(nonRaw, null, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);
|
||
const nullBoundReflectApply = Reflect.apply.bind(null);
|
||
nullBoundReflectApply(nonRaw, null, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);
|
||
const boundCall = Function.prototype.call.bind(Function.prototype.call);
|
||
boundCall(nonRaw, null, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);
|
||
Function.prototype.call.apply(nonRaw, [null, 'DELETE FROM "AuditLog" WHERE id = $1', id]);
|
||
Function.prototype.apply.apply(nonRaw, [null, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]);
|
||
Function.prototype.call.bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
Function.prototype.apply.bind(nonRaw)(null, ['UPDATE "Job" SET "status" = $1']);
|
||
const boundNonRaw = Function.prototype.call.bind(nonRaw);
|
||
boundNonRaw(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
Function.prototype.call?.bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
Function.prototype["call"].bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
Reflect["apply"](nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']);
|
||
Reflect["ap" + "ply"](nonRaw, null, ['UPDATE "Job" SET "status" = $1']);
|
||
const bracketBoundCall = Function.prototype["call"].bind(Function.prototype["call"]);
|
||
bracketBoundCall(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
const optionalBoundExec = Function.prototype.call.bind(nonRaw);
|
||
const optionalBoundReflectApply = Reflect.apply.bind(Reflect);
|
||
const optionalPlainReflectApply = Reflect.apply;
|
||
bracketBoundCall?.(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
optionalBoundExec?.(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
optionalBoundReflectApply?.(nonRaw, null, ['UPDATE "Job" SET "status" = $1']);
|
||
optionalPlainReflectApply?.(nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']);
|
||
nonRaw["call"](null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
nonRaw["apply"](null, ['UPDATE "Job" SET "status" = $1']);
|
||
nonRaw["bind"](null)('DELETE FROM "AuditLog" WHERE id = $1');
|
||
const f1 = nonRaw;
|
||
const f2 = f1;
|
||
f2('DELETE FROM "AuditLog" WHERE id = $1');
|
||
const ops = { execRaw: nonRaw };
|
||
ops.execRaw.call(null, 'DELETE FROM "AuditLog" WHERE id = $1');
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "non-raw Reflect.apply and Function.prototype.call");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/prisma/migrations/20260602000002_job_schema_ddl/migration.sql": `
|
||
CREATE TABLE "Job" ("id" TEXT PRIMARY KEY, "status" TEXT NOT NULL);
|
||
CREATE INDEX "Job_status_idx" ON "Job" ("status");
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findS1ScopeViolations(root);
|
||
assertNoViolations(violations, "Job schema DDL migration");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/creation-agent/messages/index.ts": `
|
||
export type PromptRecord = { id: string };
|
||
export type AIGameDesignDraft = { id: string };
|
||
export type DesignSection = { id: string };
|
||
export type DesignSectionPatch = { id: string };
|
||
export type GameIR = { id: string };
|
||
export type GameIRArtifact = { id: string };
|
||
export const route = "creation-agent/messages";
|
||
export const compileRoute = "compile-game-ir";
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertNoViolations(violations, "S2 approved creation workbench terms");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/game-ir/index.ts": `
|
||
export type GameIR = { id: string };
|
||
export type GameIRArtifact = { id: string };
|
||
export const compiler = "compile-game-ir";
|
||
`,
|
||
"packages/shared-contracts/src/game-ir.ts": `
|
||
export type DesignSection = { id: string };
|
||
export type DesignSectionPatch = { id: string };
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertNoViolations(violations, "S2 approved GameIR and shared-contracts paths");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/prisma.schema.spec.ts": `
|
||
export type PromptRecord = { id: string };
|
||
export type AIGameDesignDraft = { id: string };
|
||
export type DesignSection = { id: string };
|
||
export type DesignSectionPatch = { id: string };
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertNoViolations(violations, "S2 approved Prisma schema contract test terms");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/prisma.schema.spec.ts": `
|
||
export type PromptRecord = { id: string };
|
||
await db.job.update({ where: { id }, data: { status: "failed" } });
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(
|
||
violations,
|
||
"JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
"S2 Prisma schema contract test still keeps JobExecutionStore safety scanner"
|
||
);
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/simulation/game-ir.ts": "export interface GameIR { id: string }"
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "S2 blocks GameIR in simulation path");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/simulation/design-section.ts": "export interface DesignSection { id: string }"
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "S2 blocks DesignSection in simulation path");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/web-runtime/game-ir.ts": "export interface GameIR { id: string }",
|
||
"packages/shared-contracts/src/conversion/game-ir.ts": "export interface GameIRArtifact { id: string }"
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "S2 blocks approved terms in denied later-phase paths");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/web-runtime/index.ts": "export interface MiniGameProject { id: string }",
|
||
"apps/api/src/modules/events/batch.ts": 'export const route = "/events/batch";'
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(violations, "LATER_PHASE_SCOPE_LEAK", "S2 still blocks S3-S8 scope leaks");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/creation-agent/messages/job.ts":
|
||
'await db.job.update({ where: { id }, data: { status: "failed" } });'
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(
|
||
violations,
|
||
"JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE",
|
||
"S2 keeps S1 JobExecutionStore safety scanner"
|
||
);
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/creation-agent/session.ts": `
|
||
export class MainCreationAgentSessionService {}
|
||
export class AgentTaskRunner {}
|
||
`
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertNoViolations(violations, "S2 approved S1 creation agent anchors");
|
||
}
|
||
);
|
||
|
||
await withFixture(
|
||
{
|
||
"apps/api/src/modules/creation-agent/review.ts": "export class ReviewRecordWorkflow {}"
|
||
},
|
||
async (root) => {
|
||
const violations = await findScopeViolations(root, "S2");
|
||
assertHasReason(violations, "S1_ANCHOR_SCOPE_MISUSE", "S2 still blocks review/lifecycle anchors");
|
||
}
|
||
);
|
||
}
|
||
|
||
try {
|
||
const phase = parsePhaseArg(process.argv.slice(2));
|
||
await runSelfTests();
|
||
const violations = await findScopeViolations(process.cwd(), phase);
|
||
if (violations.length > 0) {
|
||
console.error(`${phase} scope check failed:`);
|
||
for (const violation of violations) {
|
||
console.error(`- ${formatViolation(violation)}`);
|
||
}
|
||
process.exitCode = 1;
|
||
} else {
|
||
console.log(`${phase} scope check passed.`);
|
||
}
|
||
} catch (error) {
|
||
console.error(error instanceof Error ? error.message : String(error));
|
||
process.exitCode = 1;
|
||
}
|