@gblikas/querykit 0.3.0 → 0.5.0

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.
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState, useCallback, useRef, JSX } from 'react';
4
4
  import { drizzle } from 'drizzle-orm/pglite';
5
5
  import { usePGlite } from '@electric-sql/pglite-react';
6
6
  import { pgTable, serial, text, integer, boolean } from 'drizzle-orm/pg-core';
7
- import { InferSelectModel, sql, SQLWrapper } from 'drizzle-orm';
7
+ import { InferSelectModel, sql } from 'drizzle-orm';
8
8
  import { Card, CardContent } from '@/components/ui/card';
9
9
  import {
10
10
  Table,
@@ -17,9 +17,9 @@ import {
17
17
  import {
18
18
  QueryParser,
19
19
  SqlTranslator,
20
- DrizzleAdapter,
21
- createQueryKit,
22
- IDrizzleAdapterOptions
20
+ createDrizzleQueryKit,
21
+ ISecurityOptions,
22
+ IQueryStructure
23
23
  } from '@gblikas/querykit';
24
24
  import { Copy, Check, Search, ChevronUp, FileCode, X } from 'lucide-react';
25
25
  import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
@@ -75,7 +75,7 @@ const highlightQueryHtml = (input: string): string => {
75
75
  .join('');
76
76
  };
77
77
 
78
- const INSTALL_SNIPPET = `pnpm i @gblikas/querykit drizzle-orm`;
78
+ const INSTALL_SNIPPET = `pnpm i @gblikas/querykit@0.3.0 drizzle-orm`;
79
79
 
80
80
  const SCHEMA_SNIPPET = `// schema.ts
81
81
  import { serial, text, pgTable } from 'drizzle-orm/pg-core';
@@ -91,25 +91,30 @@ export type SelectUser = InferSelectModel<typeof users>;
91
91
  `;
92
92
 
93
93
  const QUERYKIT_SNIPPET = `// querykit.ts
94
- import { createQueryKit } from 'querykit';
95
- import { drizzleAdapter } from 'querykit/adapters/drizzle';
94
+ import { createDrizzleQueryKit } from '@gblikas/querykit';
95
+ import { db } from './db';
96
96
  import { users } from './schema';
97
97
 
98
- export const qk = createQueryKit({
99
- adapter: drizzleAdapter,
98
+ // New 0.3.0: createDrizzleQueryKit combines adapter & security config
99
+ export const qk = createDrizzleQueryKit({
100
+ db,
100
101
  schema: { users },
102
+ security: {
103
+ maxQueryDepth: 5,
104
+ maxClauseCount: 20,
105
+ sanitizeWildcards: true,
106
+ },
101
107
  });
102
108
 
103
109
  // example.ts
104
110
  import { qk } from './querykit';
105
111
 
106
- const query = qk
112
+ const results = await qk
107
113
  .query('users')
108
114
  .where('status:done AND name:"John *"')
109
115
  .orderBy('name', 'asc')
110
- .limit(10);
111
-
112
- const results = await query.execute();
116
+ .limit(10)
117
+ .execute();
113
118
  `;
114
119
 
115
120
  const tasks = pgTable('tasks', {
@@ -137,6 +142,9 @@ export default function Home(): JSX.Element {
137
142
  const [, setLastExecutionMs] = useState<number | null>(null);
138
143
  const [rowsScanned, setRowsScanned] = useState<number | null>(null);
139
144
  const [operatorsUsed, setOperatorsUsed] = useState<string[]>([]);
145
+ const [queryStructure, setQueryStructure] = useState<IQueryStructure | null>(
146
+ null
147
+ );
140
148
  const [usedQueryKit, setUsedQueryKit] = useState<boolean>(false);
141
149
  const [, setExplainJson] = useState<string | null>(null);
142
150
  const [, setPlanningTimeMs] = useState<number | null>(null);
@@ -314,23 +322,33 @@ export default function Home(): JSX.Element {
314
322
  void seed();
315
323
  }, [db]);
316
324
 
325
+ // Security configuration for QueryKit 0.3.0
326
+ const securityOptions: ISecurityOptions = useMemo(
327
+ () => ({
328
+ maxQueryDepth: 5,
329
+ maxClauseCount: 20,
330
+ allowDotNotation: false, // Disable dot notation for simple flat schema
331
+ sanitizeWildcards: true
332
+ }),
333
+ []
334
+ );
335
+
317
336
  const parser = useMemo(() => new QueryParser(), []);
318
337
  const sqlTranslator = useMemo(
319
338
  () => new SqlTranslator({ useParameters: false }),
320
339
  []
321
340
  );
322
- const qk = useMemo(() => {
323
- const adapter = new DrizzleAdapter();
324
- const iDrizzleAdataperOptions: IDrizzleAdapterOptions = {
325
- db: db as unknown as PGlite,
326
- schema: { tasks } as unknown as Record<string, Record<string, SQLWrapper>>
327
- };
328
- adapter.initialize(iDrizzleAdataperOptions);
329
- return createQueryKit({
330
- adapter,
331
- schema: { tasks } as unknown as Record<string, Record<string, SQLWrapper>>
332
- });
333
- }, [db]);
341
+
342
+ // Use the new createDrizzleQueryKit factory (0.3.0 feature)
343
+ const qk = useMemo(
344
+ () =>
345
+ createDrizzleQueryKit({
346
+ db,
347
+ schema: { tasks },
348
+ security: securityOptions
349
+ }),
350
+ [db, securityOptions]
351
+ );
334
352
 
335
353
  // Note: Execute via QueryKit fluent API (Drizzle adapter under the hood)
336
354
 
@@ -346,6 +364,7 @@ export default function Home(): JSX.Element {
346
364
  setIsInputFocused(false);
347
365
  inputRef.current?.blur();
348
366
  setOperatorsUsed([]);
367
+ setQueryStructure(null);
349
368
  setExplainJson(null);
350
369
  setPlanningTimeMs(null);
351
370
  setExecutionTimeMs(null);
@@ -407,49 +426,77 @@ export default function Home(): JSX.Element {
407
426
  if (searchQuery.trim()) {
408
427
  try {
409
428
  const parseStart = performance.now();
410
- const ast = parser.parse(searchQuery);
411
- const translated = sqlTranslator.translate(ast) as
412
- | string
413
- | { sql: string; params: unknown[] };
414
- localParseTranslateMs = performance.now() - parseStart;
415
- setParseTranslateMs(localParseTranslateMs);
416
- whereSql =
417
- typeof translated === 'string' ? translated : translated.sql;
418
- mockSQL += ` WHERE ${whereSql}`;
419
- // Robust operator detection with word boundaries and precedence
420
- const extractOperators = (sqlText: string): string[] => {
421
- const found = new Set<string>();
422
- const upper = sqlText.toUpperCase();
423
- // Keyword operators (use word boundaries)
424
- const keywordOps: Array<[string, RegExp]> = [
425
- ['ILIKE', /\bILIKE\b/i],
426
- ['LIKE', /\bLIKE\b/i],
427
- ['AND', /\bAND\b/i],
428
- ['OR', /\bOR\b/i],
429
- ['NOT', /\bNOT\b/i],
430
- ['IN', /\bIN\b/i],
431
- ['BETWEEN', /\bBETWEEN\b/i]
432
- ];
433
- for (const [name, re] of keywordOps) {
434
- if (re.test(sqlText)) found.add(name);
429
+ // Use parseWithContext for enhanced query analysis (0.3.0 feature)
430
+ const parseResult = parser.parseWithContext(searchQuery, {
431
+ schema: {
432
+ title: { type: 'string', description: 'Task title' },
433
+ status: {
434
+ type: 'string',
435
+ description: 'Task status',
436
+ allowedValues: ['todo', 'doing', 'done']
437
+ },
438
+ priority: { type: 'number', description: 'Priority level' },
439
+ completed: { type: 'boolean', description: 'Is completed' }
440
+ }
441
+ });
442
+
443
+ // Set query structure for UI display
444
+ setQueryStructure(parseResult.structure);
445
+
446
+ if (parseResult.success && parseResult.ast) {
447
+ const translated = sqlTranslator.translate(parseResult.ast) as
448
+ | string
449
+ | { sql: string; params: unknown[] };
450
+ localParseTranslateMs = performance.now() - parseStart;
451
+ setParseTranslateMs(localParseTranslateMs);
452
+ whereSql =
453
+ typeof translated === 'string' ? translated : translated.sql;
454
+ mockSQL += ` WHERE ${whereSql}`;
455
+
456
+ // Use referenced fields from structure (0.3.0 feature)
457
+ detectedOperators = [];
458
+ if (parseResult.structure.operatorCount > 0) {
459
+ // Extract operators from the SQL for display
460
+ const extractOperators = (sqlText: string): string[] => {
461
+ const found = new Set<string>();
462
+ const keywordOps: Array<[string, RegExp]> = [
463
+ ['ILIKE', /\bILIKE\b/i],
464
+ ['LIKE', /\bLIKE\b/i],
465
+ ['AND', /\bAND\b/i],
466
+ ['OR', /\bOR\b/i],
467
+ ['NOT', /\bNOT\b/i],
468
+ ['IN', /\bIN\b/i],
469
+ ['BETWEEN', /\bBETWEEN\b/i]
470
+ ];
471
+ for (const [name, re] of keywordOps) {
472
+ if (re.test(sqlText)) found.add(name);
473
+ }
474
+ let temp = sqlText.toUpperCase();
475
+ const consume = (re: RegExp, label: string): void => {
476
+ if (re.test(temp)) {
477
+ found.add(label);
478
+ temp = temp.replace(re, ' ');
479
+ }
480
+ };
481
+ consume(/>=/g, '>=');
482
+ consume(/<=/g, '<=');
483
+ consume(/!=/g, '!=');
484
+ consume(/=/g, '=');
485
+ consume(/>/g, '>');
486
+ consume(/</g, '<');
487
+ return Array.from(found);
488
+ };
489
+ detectedOperators = extractOperators(whereSql);
490
+ } else {
491
+ // Simple query - just detect from SQL
492
+ if (/ILIKE/i.test(whereSql)) detectedOperators.push('ILIKE');
493
+ if (/=/i.test(whereSql) && !/!=/i.test(whereSql))
494
+ detectedOperators.push('=');
435
495
  }
436
- // Symbol operators: match longest first and remove before shorter matches
437
- let temp = upper;
438
- const consume = (re: RegExp, label: string): void => {
439
- if (re.test(temp)) {
440
- found.add(label);
441
- temp = temp.replace(re, ' ');
442
- }
443
- };
444
- consume(/>=/g, '>=');
445
- consume(/<=/g, '<=');
446
- consume(/!=/g, '!=');
447
- consume(/=/g, '=');
448
- consume(/>/g, '>');
449
- consume(/</g, '<');
450
- return Array.from(found);
451
- };
452
- detectedOperators = extractOperators(whereSql);
496
+ } else {
497
+ // Parse failed - fall back to ILIKE search
498
+ throw new Error(parseResult.error?.message || 'Parse failed');
499
+ }
453
500
  } catch (error) {
454
501
  void trackQueryKitIssue({
455
502
  errorName: (error as Error)?.name ?? 'UnknownError',
@@ -1013,25 +1060,77 @@ export default function Home(): JSX.Element {
1013
1060
  )}
1014
1061
  </div>
1015
1062
  {!isShortViewport ? (
1016
- <div className="mt-3">
1017
- <div className="text-xs text-muted-foreground mb-1">
1018
- Detected operators
1019
- </div>
1020
- {operatorsUsed.length ? (
1021
- <div className="flex flex-wrap gap-2">
1022
- {operatorsUsed.map(op => (
1023
- <span
1024
- key={op}
1025
- className="inline-flex items-center rounded-full border bg-muted px-2 py-0.5 text-xs font-medium"
1026
- >
1027
- {op}
1028
- </span>
1029
- ))}
1063
+ <>
1064
+ <div className="mt-3 grid grid-cols-2 gap-3">
1065
+ <div>
1066
+ <div className="text-xs text-muted-foreground mb-1">
1067
+ Detected operators
1068
+ </div>
1069
+ {operatorsUsed.length ? (
1070
+ <div className="flex flex-wrap gap-2">
1071
+ {operatorsUsed.map(op => (
1072
+ <span
1073
+ key={op}
1074
+ className="inline-flex items-center rounded-full border bg-muted px-2 py-0.5 text-xs font-medium"
1075
+ >
1076
+ {op}
1077
+ </span>
1078
+ ))}
1079
+ </div>
1080
+ ) : (
1081
+ <div className="text-xs text-muted-foreground">
1082
+ -
1083
+ </div>
1084
+ )}
1030
1085
  </div>
1031
- ) : (
1032
- <div className="text-xs text-muted-foreground">-</div>
1033
- )}
1034
- </div>
1086
+ {queryStructure && (
1087
+ <div>
1088
+ <div className="text-xs text-muted-foreground mb-1">
1089
+ Query complexity (0.3.0)
1090
+ </div>
1091
+ <div className="flex flex-wrap gap-2">
1092
+ <span
1093
+ className={cn(
1094
+ 'inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium',
1095
+ queryStructure.complexity === 'simple' &&
1096
+ 'bg-green-500/10 text-green-600 border-green-500/30',
1097
+ queryStructure.complexity === 'moderate' &&
1098
+ 'bg-yellow-500/10 text-yellow-600 border-yellow-500/30',
1099
+ queryStructure.complexity === 'complex' &&
1100
+ 'bg-red-500/10 text-red-600 border-red-500/30'
1101
+ )}
1102
+ >
1103
+ {queryStructure.complexity}
1104
+ </span>
1105
+ <span className="inline-flex items-center rounded-full border bg-muted px-2 py-0.5 text-xs font-medium">
1106
+ depth: {queryStructure.depth}
1107
+ </span>
1108
+ <span className="inline-flex items-center rounded-full border bg-muted px-2 py-0.5 text-xs font-medium">
1109
+ clauses: {queryStructure.clauseCount}
1110
+ </span>
1111
+ </div>
1112
+ </div>
1113
+ )}
1114
+ </div>
1115
+ {queryStructure &&
1116
+ queryStructure.referencedFields.length > 0 && (
1117
+ <div className="mt-3">
1118
+ <div className="text-xs text-muted-foreground mb-1">
1119
+ Referenced fields
1120
+ </div>
1121
+ <div className="flex flex-wrap gap-2">
1122
+ {queryStructure.referencedFields.map(field => (
1123
+ <span
1124
+ key={field}
1125
+ className="inline-flex items-center rounded-full border bg-blue-500/10 text-blue-600 border-blue-500/30 px-2 py-0.5 text-xs font-medium"
1126
+ >
1127
+ {field}
1128
+ </span>
1129
+ ))}
1130
+ </div>
1131
+ </div>
1132
+ )}
1133
+ </>
1035
1134
  ) : (
1036
1135
  <div className="mt-3 text-xs text-muted-foreground">
1037
1136
  View on larger screen for more details
@@ -11,7 +11,7 @@
11
11
  "dependencies": {
12
12
  "@electric-sql/pglite": "^0.3.7",
13
13
  "@electric-sql/pglite-react": "^0.2.25",
14
- "@gblikas/querykit": "^0.0.0",
14
+ "@gblikas/querykit": "^0.3.0",
15
15
  "@radix-ui/react-dialog": "^1.1.15",
16
16
  "@radix-ui/react-hover-card": "^1.1.15",
17
17
  "@vercel/analytics": "^1.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gblikas/querykit",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A comprehensive query toolkit for TypeScript that simplifies building and executing data queries across different environments",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "lint-staged": "^15.5.1",
44
44
  "prettier": "^3.2.5",
45
45
  "ts-jest": "^29.1.2",
46
- "typescript": "^5.3.3"
46
+ "typescript": "^5.9.3"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc --outDir dist",
@@ -9,6 +9,7 @@ import { IAdapter, IAdapterOptions, IQueryExecutionOptions } from '../types';
9
9
  import { QueryExpression } from '../../parser/types';
10
10
  import { SQL, SQLWrapper, asc, desc, sql } from 'drizzle-orm';
11
11
  import { createQueryKit, QueryKit } from '../../index';
12
+ import { IQueryContext } from '../../virtual-fields';
12
13
  /**
13
14
  * Type for Drizzle ORM database instance
14
15
  */
@@ -91,8 +92,7 @@ export class DrizzleAdapterError extends Error {
91
92
  */
92
93
  export class DrizzleAdapter<
93
94
  TSchema extends Record<string, unknown> = Record<string, unknown>
94
- > implements IAdapter<IDrizzleAdapterOptions<TSchema>>
95
- {
95
+ > implements IAdapter<IDrizzleAdapterOptions<TSchema>> {
96
96
  private db!: unknown;
97
97
  private schema!: TSchema;
98
98
  private translator!: DrizzleTranslator;
@@ -291,7 +291,7 @@ export function createDrizzleQueryKit<
291
291
 
292
292
  type RowMap = RowMapFromDrizzleSchema<TSchema>;
293
293
 
294
- return createQueryKit<TSchema, RowMap>({
294
+ return createQueryKit<TSchema, IQueryContext, RowMap>({
295
295
  adapter,
296
296
  schema: args.schema as unknown as TSchema,
297
297
  security: args.security
package/src/index.ts CHANGED
@@ -12,6 +12,11 @@ import { QueryParser, IParserOptions } from './parser';
12
12
  import { SqlTranslator } from './translators/sql';
13
13
  import { ISecurityOptions, QuerySecurityValidator } from './security';
14
14
  import { IAdapter, IAdapterOptions } from './adapters';
15
+ import {
16
+ IQueryContext,
17
+ VirtualFieldsConfig,
18
+ resolveVirtualFields
19
+ } from './virtual-fields';
15
20
 
16
21
  export {
17
22
  // Parser exports
@@ -29,6 +34,7 @@ export {
29
34
  // Re-export from modules
30
35
  export * from './translators';
31
36
  export * from './adapters';
37
+ export * from './virtual-fields';
32
38
 
33
39
  /**
34
40
  * Create a new QueryBuilder instance
@@ -53,7 +59,8 @@ export interface IQueryKitOptions<
53
59
  TSchema extends Record<string, object> = Record<
54
60
  string,
55
61
  Record<string, unknown>
56
- >
62
+ >,
63
+ TContext extends IQueryContext = IQueryContext
57
64
  > {
58
65
  /**
59
66
  * The adapter to use for database connections
@@ -74,6 +81,39 @@ export interface IQueryKitOptions<
74
81
  * Options to initialize the provided adapter
75
82
  */
76
83
  adapterOptions?: IAdapterOptions & { [key: string]: unknown };
84
+
85
+ /**
86
+ * Virtual field definitions for context-aware query expansion.
87
+ * Virtual fields allow shortcuts like `my:assigned` that expand to
88
+ * real schema fields at query execution time.
89
+ *
90
+ * @example
91
+ * virtualFields: {
92
+ * my: {
93
+ * allowedValues: ['assigned', 'created'] as const,
94
+ * resolve: (input, ctx, { fields }) => ({
95
+ * type: 'comparison',
96
+ * field: fields({ assigned: 'assignee_id', created: 'creator_id' })[input.value],
97
+ * operator: '==',
98
+ * value: ctx.currentUserId
99
+ * })
100
+ * }
101
+ * }
102
+ */
103
+ virtualFields?: VirtualFieldsConfig<TSchema, TContext>;
104
+
105
+ /**
106
+ * Factory function to create query execution context.
107
+ * Called once per query execution to provide runtime values
108
+ * for virtual field resolution.
109
+ *
110
+ * @example
111
+ * createContext: async () => ({
112
+ * currentUserId: await getCurrentUserId(),
113
+ * currentUserTeamIds: await getUserTeamIds()
114
+ * })
115
+ */
116
+ createContext?: () => TContext | Promise<TContext>;
77
117
  }
78
118
 
79
119
  // Define interfaces for return types
@@ -105,10 +145,11 @@ export type QueryKit<
105
145
  */
106
146
  export function createQueryKit<
107
147
  TSchema extends Record<string, object>,
148
+ TContext extends IQueryContext = IQueryContext,
108
149
  TRows extends { [K in keyof TSchema & string]: unknown } = {
109
150
  [K in keyof TSchema & string]: unknown;
110
151
  }
111
- >(options: IQueryKitOptions<TSchema>): QueryKit<TSchema, TRows> {
152
+ >(options: IQueryKitOptions<TSchema, TContext>): QueryKit<TSchema, TRows> {
112
153
  const parser = new QueryParser();
113
154
  const securityValidator = new QuerySecurityValidator(options.security);
114
155
 
@@ -136,12 +177,8 @@ export function createQueryKit<
136
177
  ): IWhereClause<TRows[K]> => {
137
178
  return {
138
179
  where: (queryString: string): IQueryExecutor<TRows[K]> => {
139
- // Parse and validate the query
180
+ // Parse the query
140
181
  const expressionAst = parser.parse(queryString);
141
- securityValidator.validate(
142
- expressionAst,
143
- options.schema as unknown as Record<string, Record<string, unknown>>
144
- );
145
182
 
146
183
  // Execution state accumulated via fluent calls
147
184
  let orderByState: Record<string, 'asc' | 'desc'> = {};
@@ -165,10 +202,42 @@ export function createQueryKit<
165
202
  return executor;
166
203
  },
167
204
  execute: async (): Promise<TRows[K][]> => {
205
+ // Validate that if virtual fields are configured, createContext must also be provided
206
+ if (options.virtualFields && !options.createContext) {
207
+ throw new Error(
208
+ 'createContext must be provided when virtualFields is configured'
209
+ );
210
+ }
211
+
212
+ // Get context if virtual fields are configured
213
+ let context: TContext | undefined;
214
+ if (options.virtualFields && options.createContext) {
215
+ context = await options.createContext();
216
+ }
217
+
218
+ // Resolve virtual fields if configured and context is available
219
+ let resolvedExpression = expressionAst;
220
+ if (options.virtualFields && context) {
221
+ resolvedExpression = resolveVirtualFields(
222
+ expressionAst,
223
+ options.virtualFields,
224
+ context
225
+ );
226
+ }
227
+
228
+ // Validate the resolved query
229
+ securityValidator.validate(
230
+ resolvedExpression,
231
+ options.schema as unknown as Record<
232
+ string,
233
+ Record<string, unknown>
234
+ >
235
+ );
236
+
168
237
  // Delegate to adapter
169
238
  const results = await options.adapter.execute(
170
239
  table,
171
- expressionAst,
240
+ resolvedExpression,
172
241
  {
173
242
  orderBy:
174
243
  Object.keys(orderByState).length > 0
@@ -51,10 +51,30 @@ export interface ILogicalExpression {
51
51
  right?: QueryExpression;
52
52
  }
53
53
 
54
+ /**
55
+ * Represents a raw SQL expression node in the AST.
56
+ * Used by virtual fields to inject database-specific SQL operations.
57
+ */
58
+ export interface IRawSqlExpression {
59
+ type: 'raw';
60
+ /**
61
+ * Function that generates the raw SQL for the adapter.
62
+ * For Drizzle, this should return a SQL template result.
63
+ */
64
+ toSql: (context: {
65
+ adapter: string;
66
+ tableName: string;
67
+ schema: Record<string, unknown>;
68
+ }) => unknown;
69
+ }
70
+
54
71
  /**
55
72
  * Represents any valid query expression node
56
73
  */
57
- export type QueryExpression = IComparisonExpression | ILogicalExpression;
74
+ export type QueryExpression =
75
+ | IComparisonExpression
76
+ | ILogicalExpression
77
+ | IRawSqlExpression;
58
78
 
59
79
  /**
60
80
  * Configuration options for the parser
@@ -240,13 +240,14 @@ export class QuerySecurityValidator {
240
240
  `Found "${field}" - use a simple field name without dots instead.`
241
241
  );
242
242
  }
243
- } else {
243
+ } else if (expression.type === 'logical') {
244
244
  // Recursively validate logical expressions
245
245
  this.validateNoDotNotation(expression.left);
246
246
  if (expression.right) {
247
247
  this.validateNoDotNotation(expression.right);
248
248
  }
249
249
  }
250
+ // Raw expressions are skipped - they handle their own field access
250
251
  }
251
252
 
252
253
  /**
@@ -285,13 +286,14 @@ export class QuerySecurityValidator {
285
286
  }
286
287
  }
287
288
  }
288
- } else {
289
+ } else if (expression.type === 'logical') {
289
290
  // Recursively validate logical expressions
290
291
  this.validateDenyValues(expression.left);
291
292
  if (expression.right) {
292
293
  this.validateDenyValues(expression.right);
293
294
  }
294
295
  }
296
+ // Raw expressions are skipped - they handle their own values
295
297
  }
296
298
 
297
299
  /**
@@ -350,6 +352,7 @@ export class QuerySecurityValidator {
350
352
  this.validateQueryDepth(expression.right, currentDepth + 1);
351
353
  }
352
354
  }
355
+ // Raw and comparison expressions don't add depth
353
356
  }
354
357
 
355
358
  /**
@@ -379,6 +382,10 @@ export class QuerySecurityValidator {
379
382
  return 1;
380
383
  }
381
384
 
385
+ if (expression.type === 'raw') {
386
+ return 1; // Raw expressions count as one clause
387
+ }
388
+
382
389
  let count = 0;
383
390
  count += this.countClauses(expression.left);
384
391
  if (expression.right) {
@@ -442,12 +449,13 @@ export class QuerySecurityValidator {
442
449
  ) {
443
450
  throw new QuerySecurityError('Object values are not allowed');
444
451
  }
445
- } else {
452
+ } else if (expression.type === 'logical') {
446
453
  this.validateValueLengths(expression.left);
447
454
  if (expression.right) {
448
455
  this.validateValueLengths(expression.right);
449
456
  }
450
457
  }
458
+ // Raw expressions are skipped - they handle their own values
451
459
  }
452
460
 
453
461
  /**
@@ -481,12 +489,13 @@ export class QuerySecurityValidator {
481
489
  .replace(/\?{2,}/g, '?'); // Limit consecutive question marks
482
490
  (expression as IComparisonExpression).value = sanitized;
483
491
  }
484
- } else {
492
+ } else if (expression.type === 'logical') {
485
493
  this.sanitizeWildcards(expression.left);
486
494
  if (expression.right) {
487
495
  this.sanitizeWildcards(expression.right);
488
496
  }
489
497
  }
498
+ // Raw expressions are skipped - they handle their own wildcards
490
499
  }
491
500
 
492
501
  /**
@@ -502,11 +511,12 @@ export class QuerySecurityValidator {
502
511
  ): void {
503
512
  if (expression.type === 'comparison') {
504
513
  fieldSet.add(expression.field);
505
- } else {
514
+ } else if (expression.type === 'logical') {
506
515
  this.collectFields(expression.left, fieldSet);
507
516
  if (expression.right) {
508
517
  this.collectFields(expression.right, fieldSet);
509
518
  }
510
519
  }
520
+ // Raw expressions don't expose field names for collection
511
521
  }
512
522
  }