@prisma-next/runtime-executor 0.3.0-dev.3 → 0.3.0-dev.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import type { ExecutionPlan, PlanMeta, PlanRefs } from '@prisma-next/contract/types';
2
+
3
+ export type LintSeverity = 'error' | 'warn';
4
+ export type BudgetSeverity = 'error' | 'warn';
5
+
6
+ export interface LintFinding {
7
+ readonly code: `LINT.${string}`;
8
+ readonly severity: LintSeverity;
9
+ readonly message: string;
10
+ readonly details?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface BudgetFinding {
14
+ readonly code: `BUDGET.${string}`;
15
+ readonly severity: BudgetSeverity;
16
+ readonly message: string;
17
+ readonly details?: Record<string, unknown>;
18
+ }
19
+
20
+ export interface RawGuardrailConfig {
21
+ readonly budgets?: {
22
+ readonly unboundedSelectSeverity?: BudgetSeverity;
23
+ readonly estimatedRows?: number;
24
+ };
25
+ }
26
+
27
+ export interface RawGuardrailResult {
28
+ readonly lints: LintFinding[];
29
+ readonly budgets: BudgetFinding[];
30
+ readonly statement: 'select' | 'mutation' | 'other';
31
+ }
32
+
33
+ const SELECT_STAR_REGEX = /select\s+\*/i;
34
+ const LIMIT_REGEX = /\blimit\b/i;
35
+ const MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
36
+
37
+ const READ_ONLY_INTENTS = new Set(['read', 'report', 'readonly']);
38
+
39
+ export function evaluateRawGuardrails(
40
+ plan: ExecutionPlan,
41
+ config?: RawGuardrailConfig,
42
+ ): RawGuardrailResult {
43
+ const lints: LintFinding[] = [];
44
+ const budgets: BudgetFinding[] = [];
45
+
46
+ const normalized = normalizeWhitespace(plan.sql);
47
+ const statementType = classifyStatement(normalized);
48
+
49
+ if (statementType === 'select') {
50
+ if (SELECT_STAR_REGEX.test(normalized)) {
51
+ lints.push(
52
+ createLint('LINT.SELECT_STAR', 'error', 'Raw SQL plan selects all columns via *', {
53
+ sql: snippet(plan.sql),
54
+ }),
55
+ );
56
+ }
57
+
58
+ if (!LIMIT_REGEX.test(normalized)) {
59
+ const severity = config?.budgets?.unboundedSelectSeverity ?? 'error';
60
+ lints.push(
61
+ createLint('LINT.NO_LIMIT', 'warn', 'Raw SQL plan omits LIMIT clause', {
62
+ sql: snippet(plan.sql),
63
+ }),
64
+ );
65
+
66
+ budgets.push(
67
+ createBudget(
68
+ 'BUDGET.ROWS_EXCEEDED',
69
+ severity,
70
+ 'Raw SQL plan is unbounded and may exceed row budget',
71
+ {
72
+ sql: snippet(plan.sql),
73
+ ...(config?.budgets?.estimatedRows !== undefined
74
+ ? { estimatedRows: config.budgets.estimatedRows }
75
+ : {}),
76
+ },
77
+ ),
78
+ );
79
+ }
80
+ }
81
+
82
+ if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) {
83
+ lints.push(
84
+ createLint(
85
+ 'LINT.READ_ONLY_MUTATION',
86
+ 'error',
87
+ 'Raw SQL plan mutates data despite read-only intent',
88
+ {
89
+ sql: snippet(plan.sql),
90
+ intent: plan.meta.annotations?.['intent'],
91
+ },
92
+ ),
93
+ );
94
+ }
95
+
96
+ const refs = plan.meta.refs;
97
+ if (refs) {
98
+ evaluateIndexCoverage(refs, lints);
99
+ }
100
+
101
+ return { lints, budgets, statement: statementType };
102
+ }
103
+
104
+ function evaluateIndexCoverage(refs: PlanRefs, lints: LintFinding[]) {
105
+ const predicateColumns = refs.columns ?? [];
106
+ if (predicateColumns.length === 0) {
107
+ return;
108
+ }
109
+
110
+ const indexes = refs.indexes ?? [];
111
+
112
+ if (indexes.length === 0) {
113
+ lints.push(
114
+ createLint(
115
+ 'LINT.UNINDEXED_PREDICATE',
116
+ 'warn',
117
+ 'Raw SQL plan predicates lack supporting indexes',
118
+ {
119
+ predicates: predicateColumns,
120
+ },
121
+ ),
122
+ );
123
+ return;
124
+ }
125
+
126
+ const hasSupportingIndex = predicateColumns.every((column) =>
127
+ indexes.some(
128
+ (index) =>
129
+ index.table === column.table &&
130
+ index.columns.some((col) => col.toLowerCase() === column.column.toLowerCase()),
131
+ ),
132
+ );
133
+
134
+ if (!hasSupportingIndex) {
135
+ lints.push(
136
+ createLint(
137
+ 'LINT.UNINDEXED_PREDICATE',
138
+ 'warn',
139
+ 'Raw SQL plan predicates lack supporting indexes',
140
+ {
141
+ predicates: predicateColumns,
142
+ },
143
+ ),
144
+ );
145
+ }
146
+ }
147
+
148
+ function classifyStatement(sql: string): 'select' | 'mutation' | 'other' {
149
+ const trimmed = sql.trim();
150
+ const lower = trimmed.toLowerCase();
151
+
152
+ if (lower.startsWith('with')) {
153
+ if (lower.includes('select')) {
154
+ return 'select';
155
+ }
156
+ }
157
+
158
+ if (lower.startsWith('select')) {
159
+ return 'select';
160
+ }
161
+
162
+ if (MUTATION_PREFIX_REGEX.test(trimmed)) {
163
+ return 'mutation';
164
+ }
165
+
166
+ return 'other';
167
+ }
168
+
169
+ function isMutationStatement(statement: 'select' | 'mutation' | 'other'): boolean {
170
+ return statement === 'mutation';
171
+ }
172
+
173
+ function isReadOnlyIntent(meta: PlanMeta): boolean {
174
+ const annotations = meta.annotations as { intent?: string } | undefined;
175
+ const intent =
176
+ typeof annotations?.intent === 'string' ? annotations.intent.toLowerCase() : undefined;
177
+ return intent !== undefined && READ_ONLY_INTENTS.has(intent);
178
+ }
179
+
180
+ function normalizeWhitespace(value: string): string {
181
+ return value.replace(/\s+/g, ' ').trim();
182
+ }
183
+
184
+ function snippet(sql: string): string {
185
+ return normalizeWhitespace(sql).slice(0, 200);
186
+ }
187
+
188
+ function createLint(
189
+ code: LintFinding['code'],
190
+ severity: LintFinding['severity'],
191
+ message: string,
192
+ details?: Record<string, unknown>,
193
+ ): LintFinding {
194
+ return { code, severity, message, ...(details ? { details } : {}) };
195
+ }
196
+
197
+ function createBudget(
198
+ code: BudgetFinding['code'],
199
+ severity: BudgetFinding['severity'],
200
+ message: string,
201
+ details?: Record<string, unknown>,
202
+ ): BudgetFinding {
203
+ return { code, severity, message, ...(details ? { details } : {}) };
204
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './exports';
package/src/marker.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
+ import { type } from 'arktype';
3
+
4
+ // Re-export for backward compatibility
5
+ export type { ContractMarkerRecord } from '@prisma-next/contract/types';
6
+
7
+ export interface ContractMarkerRow {
8
+ core_hash: string;
9
+ profile_hash: string;
10
+ contract_json: unknown | null;
11
+ canonical_version: number | null;
12
+ updated_at: Date;
13
+ app_tag: string | null;
14
+ meta: unknown | null;
15
+ }
16
+
17
+ const MetaSchema = type({ '[string]': 'unknown' });
18
+
19
+ function parseMeta(meta: unknown): Record<string, unknown> {
20
+ if (meta === null || meta === undefined) {
21
+ return {};
22
+ }
23
+
24
+ let parsed: unknown;
25
+ if (typeof meta === 'string') {
26
+ try {
27
+ parsed = JSON.parse(meta);
28
+ } catch {
29
+ return {};
30
+ }
31
+ } else {
32
+ parsed = meta;
33
+ }
34
+
35
+ const result = MetaSchema(parsed);
36
+ if (result instanceof type.errors) {
37
+ return {};
38
+ }
39
+
40
+ return result as Record<string, unknown>;
41
+ }
42
+
43
+ const ContractMarkerRowSchema = type({
44
+ core_hash: 'string',
45
+ profile_hash: 'string',
46
+ 'contract_json?': 'unknown | null',
47
+ 'canonical_version?': 'number | null',
48
+ 'updated_at?': 'Date | string',
49
+ 'app_tag?': 'string | null',
50
+ 'meta?': 'unknown | null',
51
+ });
52
+
53
+ export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
54
+ const result = ContractMarkerRowSchema(row);
55
+ if (result instanceof type.errors) {
56
+ const messages = result.map((p: { message: string }) => p.message).join('; ');
57
+ throw new Error(`Invalid contract marker row: ${messages}`);
58
+ }
59
+
60
+ const validatedRow = result as {
61
+ core_hash: string;
62
+ profile_hash: string;
63
+ contract_json?: unknown | null;
64
+ canonical_version?: number | null;
65
+ updated_at?: Date | string;
66
+ app_tag?: string | null;
67
+ meta?: unknown | null;
68
+ };
69
+
70
+ const updatedAt = validatedRow.updated_at
71
+ ? validatedRow.updated_at instanceof Date
72
+ ? validatedRow.updated_at
73
+ : new Date(validatedRow.updated_at)
74
+ : new Date();
75
+
76
+ return {
77
+ coreHash: validatedRow.core_hash,
78
+ profileHash: validatedRow.profile_hash,
79
+ contractJson: validatedRow.contract_json ?? null,
80
+ canonicalVersion: validatedRow.canonical_version ?? null,
81
+ updatedAt,
82
+ appTag: validatedRow.app_tag ?? null,
83
+ meta: parseMeta(validatedRow.meta),
84
+ };
85
+ }
@@ -0,0 +1,328 @@
1
+ import type { ExecutionPlan } from '@prisma-next/contract/types';
2
+ import type { AfterExecuteResult, Plugin, PluginContext } from './types';
3
+
4
+ export interface BudgetsOptions {
5
+ readonly maxRows?: number;
6
+ readonly defaultTableRows?: number;
7
+ readonly tableRows?: Record<string, number>;
8
+ readonly maxLatencyMs?: number;
9
+ readonly severities?: {
10
+ readonly rowCount?: 'warn' | 'error';
11
+ readonly latency?: 'warn' | 'error';
12
+ };
13
+ readonly explain?: {
14
+ readonly enabled?: boolean;
15
+ };
16
+ }
17
+
18
+ interface DriverWithExplain {
19
+ explain?(
20
+ sql: string,
21
+ params: unknown[],
22
+ ): Promise<{ rows: ReadonlyArray<Record<string, unknown>> }>;
23
+ }
24
+
25
+ async function computeEstimatedRows(
26
+ plan: ExecutionPlan,
27
+ driver: DriverWithExplain,
28
+ ): Promise<number | undefined> {
29
+ if (typeof driver.explain !== 'function') {
30
+ return undefined;
31
+ }
32
+
33
+ try {
34
+ const result = await driver.explain(plan.sql, [...plan.params]);
35
+ return extractEstimatedRows(result.rows);
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ function extractEstimatedRows(rows: ReadonlyArray<Record<string, unknown>>): number | undefined {
42
+ for (const row of rows) {
43
+ const estimate = findPlanRows(row);
44
+ if (estimate !== undefined) {
45
+ return estimate;
46
+ }
47
+ }
48
+
49
+ return undefined;
50
+ }
51
+
52
+ type ExplainNode = {
53
+ Plan?: unknown;
54
+ Plans?: unknown[];
55
+ 'Plan Rows'?: number;
56
+ [key: string]: unknown;
57
+ };
58
+
59
+ function findPlanRows(node: unknown): number | undefined {
60
+ if (!node || typeof node !== 'object') {
61
+ return undefined;
62
+ }
63
+
64
+ const explainNode = node as ExplainNode;
65
+ const planRows = explainNode['Plan Rows'];
66
+ if (typeof planRows === 'number') {
67
+ return planRows;
68
+ }
69
+
70
+ if ('Plan' in explainNode && explainNode.Plan !== undefined) {
71
+ const nested = findPlanRows(explainNode.Plan);
72
+ if (nested !== undefined) {
73
+ return nested;
74
+ }
75
+ }
76
+
77
+ if (Array.isArray(explainNode.Plans)) {
78
+ for (const child of explainNode.Plans) {
79
+ const nested = findPlanRows(child);
80
+ if (nested !== undefined) {
81
+ return nested;
82
+ }
83
+ }
84
+ }
85
+
86
+ for (const value of Object.values(node as Record<string, unknown>)) {
87
+ if (typeof value === 'object' && value !== null) {
88
+ const nested = findPlanRows(value);
89
+ if (nested !== undefined) {
90
+ return nested;
91
+ }
92
+ }
93
+ }
94
+
95
+ return undefined;
96
+ }
97
+
98
+ function budgetError(code: string, message: string, details?: Record<string, unknown>) {
99
+ const error = new Error(message) as Error & {
100
+ code: string;
101
+ category: 'BUDGET';
102
+ severity: 'error';
103
+ details?: Record<string, unknown>;
104
+ };
105
+ Object.defineProperty(error, 'name', {
106
+ value: 'RuntimeError',
107
+ configurable: true,
108
+ });
109
+ return Object.assign(error, {
110
+ code,
111
+ category: 'BUDGET' as const,
112
+ severity: 'error' as const,
113
+ details,
114
+ });
115
+ }
116
+
117
+ function estimateRows(
118
+ plan: ExecutionPlan,
119
+ tableRows: Record<string, number>,
120
+ defaultTableRows: number,
121
+ ): number | null {
122
+ if (!plan.ast) {
123
+ return null;
124
+ }
125
+
126
+ const table = plan.meta.refs?.tables?.[0];
127
+ if (!table) {
128
+ return null;
129
+ }
130
+
131
+ const tableEstimate = tableRows[table] ?? defaultTableRows;
132
+
133
+ if (
134
+ plan.ast &&
135
+ typeof plan.ast === 'object' &&
136
+ 'kind' in plan.ast &&
137
+ plan.ast.kind === 'select' &&
138
+ 'limit' in plan.ast &&
139
+ typeof plan.ast.limit === 'number'
140
+ ) {
141
+ return Math.min(plan.ast.limit, tableEstimate);
142
+ }
143
+
144
+ return tableEstimate;
145
+ }
146
+
147
+ function hasDetectableLimit(plan: ExecutionPlan): boolean {
148
+ if (
149
+ plan.ast &&
150
+ typeof plan.ast === 'object' &&
151
+ 'kind' in plan.ast &&
152
+ plan.ast.kind === 'select' &&
153
+ 'limit' in plan.ast &&
154
+ typeof plan.ast.limit === 'number'
155
+ ) {
156
+ return true;
157
+ }
158
+
159
+ const annotations = plan.meta.annotations as { limit?: number; LIMIT?: number } | undefined;
160
+ return typeof annotations?.limit === 'number' || typeof annotations?.LIMIT === 'number';
161
+ }
162
+
163
+ export function budgets<TContract = unknown, TAdapter = unknown, TDriver = unknown>(
164
+ options?: BudgetsOptions,
165
+ ): Plugin<TContract, TAdapter, TDriver> {
166
+ const maxRows = options?.maxRows ?? 10_000;
167
+ const defaultTableRows = options?.defaultTableRows ?? 10_000;
168
+ const tableRows = options?.tableRows ?? {};
169
+ const maxLatencyMs = options?.maxLatencyMs ?? 1_000;
170
+ const rowSeverity = options?.severities?.rowCount ?? 'error';
171
+ const latencySeverity = options?.severities?.latency ?? 'warn';
172
+
173
+ let observedRows = 0;
174
+
175
+ return Object.freeze({
176
+ name: 'budgets',
177
+
178
+ async beforeExecute(plan: ExecutionPlan, ctx: PluginContext<TContract, TAdapter, TDriver>) {
179
+ observedRows = 0;
180
+ void ctx.now();
181
+
182
+ const estimated = estimateRows(plan, tableRows, defaultTableRows);
183
+ const isUnbounded = !hasDetectableLimit(plan);
184
+ const sqlUpper = plan.sql.trimStart().toUpperCase();
185
+ const isSelect = sqlUpper.startsWith('SELECT');
186
+
187
+ // Check for unbounded queries first - these should always error if they exceed or equal the budget
188
+ if (isSelect && isUnbounded) {
189
+ if (estimated !== null && estimated >= maxRows) {
190
+ const error = budgetError(
191
+ 'BUDGET.ROWS_EXCEEDED',
192
+ 'Unbounded SELECT query exceeds budget',
193
+ {
194
+ source: 'heuristic',
195
+ estimatedRows: estimated,
196
+ maxRows,
197
+ },
198
+ );
199
+
200
+ const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
201
+ if (shouldBlock) {
202
+ throw error;
203
+ }
204
+ ctx.log.warn({
205
+ code: error.code,
206
+ message: error.message,
207
+ details: error.details,
208
+ });
209
+ return;
210
+ }
211
+
212
+ // Even if we can't estimate, unbounded queries should error
213
+ const error = budgetError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', {
214
+ source: 'heuristic',
215
+ maxRows,
216
+ });
217
+
218
+ const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
219
+ if (shouldBlock) {
220
+ throw error;
221
+ }
222
+ ctx.log.warn({
223
+ code: error.code,
224
+ message: error.message,
225
+ details: error.details,
226
+ });
227
+ return;
228
+ }
229
+
230
+ // For bounded queries, check if estimated exceeds budget
231
+ if (estimated !== null) {
232
+ if (estimated > maxRows) {
233
+ const error = budgetError('BUDGET.ROWS_EXCEEDED', 'Estimated row count exceeds budget', {
234
+ source: 'heuristic',
235
+ estimatedRows: estimated,
236
+ maxRows,
237
+ });
238
+
239
+ const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
240
+ if (shouldBlock) {
241
+ throw error;
242
+ }
243
+ ctx.log.warn({
244
+ code: error.code,
245
+ message: error.message,
246
+ details: error.details,
247
+ });
248
+ }
249
+ return;
250
+ }
251
+
252
+ // Fallback: if no AST, try EXPLAIN if enabled
253
+ if (!plan.ast) {
254
+ const explainEnabled = options?.explain?.enabled === true;
255
+
256
+ if (explainEnabled && isSelect && typeof ctx.driver === 'object' && ctx.driver !== null) {
257
+ const estimatedRows = await computeEstimatedRows(plan, ctx.driver as DriverWithExplain);
258
+ if (estimatedRows !== undefined) {
259
+ if (estimatedRows > maxRows) {
260
+ const error = budgetError(
261
+ 'BUDGET.ROWS_EXCEEDED',
262
+ 'Estimated row count exceeds budget',
263
+ {
264
+ source: 'explain',
265
+ estimatedRows,
266
+ maxRows,
267
+ },
268
+ );
269
+
270
+ const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
271
+ if (shouldBlock) {
272
+ throw error;
273
+ }
274
+ ctx.log.warn({
275
+ code: error.code,
276
+ message: error.message,
277
+ details: error.details,
278
+ });
279
+ }
280
+ return;
281
+ }
282
+ }
283
+ }
284
+ },
285
+
286
+ async onRow(
287
+ _row: Record<string, unknown>,
288
+ _plan: ExecutionPlan,
289
+ _ctx: PluginContext<TContract, TAdapter, TDriver>,
290
+ ) {
291
+ void _row;
292
+ void _plan;
293
+ void _ctx;
294
+ observedRows += 1;
295
+ if (observedRows > maxRows) {
296
+ throw budgetError('BUDGET.ROWS_EXCEEDED', 'Observed row count exceeds budget', {
297
+ source: 'observed',
298
+ observedRows,
299
+ maxRows,
300
+ });
301
+ }
302
+ },
303
+
304
+ async afterExecute(
305
+ _plan: ExecutionPlan,
306
+ result: AfterExecuteResult,
307
+ ctx: PluginContext<TContract, TAdapter, TDriver>,
308
+ ) {
309
+ const latencyMs = result.latencyMs;
310
+ if (latencyMs > maxLatencyMs) {
311
+ const error = budgetError('BUDGET.TIME_EXCEEDED', 'Query latency exceeds budget', {
312
+ latencyMs,
313
+ maxLatencyMs,
314
+ });
315
+
316
+ const shouldBlock = latencySeverity === 'error' && ctx.mode === 'strict';
317
+ if (shouldBlock) {
318
+ throw error;
319
+ }
320
+ ctx.log.warn({
321
+ code: error.code,
322
+ message: error.message,
323
+ details: error.details,
324
+ });
325
+ }
326
+ },
327
+ });
328
+ }
@@ -0,0 +1,85 @@
1
+ import type { ExecutionPlan } from '@prisma-next/contract/types';
2
+ import { evaluateRawGuardrails } from '../guardrails/raw';
3
+ import type { Plugin, PluginContext } from './types';
4
+
5
+ export interface LintsOptions {
6
+ readonly severities?: {
7
+ readonly selectStar?: 'warn' | 'error';
8
+ readonly noLimit?: 'warn' | 'error';
9
+ readonly readOnlyMutation?: 'warn' | 'error';
10
+ readonly unindexedPredicate?: 'warn' | 'error';
11
+ };
12
+ }
13
+
14
+ function lintError(code: string, message: string, details?: Record<string, unknown>) {
15
+ const error = new Error(message) as Error & {
16
+ code: string;
17
+ category: 'LINT';
18
+ severity: 'error';
19
+ details?: Record<string, unknown>;
20
+ };
21
+ Object.defineProperty(error, 'name', {
22
+ value: 'RuntimeError',
23
+ configurable: true,
24
+ });
25
+ return Object.assign(error, {
26
+ code,
27
+ category: 'LINT' as const,
28
+ severity: 'error' as const,
29
+ details,
30
+ });
31
+ }
32
+
33
+ export function lints<TContract = unknown, TAdapter = unknown, TDriver = unknown>(
34
+ options?: LintsOptions,
35
+ ): Plugin<TContract, TAdapter, TDriver> {
36
+ return Object.freeze({
37
+ name: 'lints',
38
+
39
+ async beforeExecute(plan: ExecutionPlan, ctx: PluginContext<TContract, TAdapter, TDriver>) {
40
+ if (plan.ast) {
41
+ return;
42
+ }
43
+
44
+ const evaluation = evaluateRawGuardrails(plan);
45
+
46
+ for (const lint of evaluation.lints) {
47
+ const configuredSeverity = getConfiguredSeverity(lint.code, options);
48
+ const effectiveSeverity = configuredSeverity ?? lint.severity;
49
+
50
+ if (effectiveSeverity === 'error') {
51
+ throw lintError(lint.code, lint.message, lint.details);
52
+ }
53
+ if (effectiveSeverity === 'warn') {
54
+ ctx.log.warn({
55
+ code: lint.code,
56
+ message: lint.message,
57
+ details: lint.details,
58
+ });
59
+ }
60
+ }
61
+ },
62
+ });
63
+ }
64
+
65
+ function getConfiguredSeverity(code: string, options?: LintsOptions): 'warn' | 'error' | undefined {
66
+ const severities = options?.severities;
67
+ if (!severities) {
68
+ return undefined;
69
+ }
70
+
71
+ if (code === 'LINT.SELECT_STAR') {
72
+ return severities.selectStar;
73
+ }
74
+ if (code === 'LINT.NO_LIMIT') {
75
+ return severities.noLimit;
76
+ }
77
+ if (code === 'LINT.READ_ONLY_MUTATION') {
78
+ return severities.readOnlyMutation;
79
+ }
80
+ if (code === 'LINT.UNINDEXED_PREDICATE') {
81
+ return severities.unindexedPredicate;
82
+ }
83
+
84
+ return undefined;
85
+ }