@inixiative/json-rules 1.1.2 → 1.2.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.
package/dist/index.d.cts CHANGED
@@ -1,119 +1,350 @@
1
- declare enum Operator {
2
- equals = "equals",
3
- notEquals = "notEquals",
4
- lessThan = "lessThan",
5
- lessThanEquals = "lessThanEquals",
6
- greaterThan = "greaterThan",
7
- greaterThanEquals = "greaterThanEquals",
8
- contains = "contains",
9
- notContains = "notContains",
10
- in = "in",
11
- notIn = "notIn",
12
- matches = "matches",
13
- notMatches = "notMatches",
14
- between = "between",
15
- notBetween = "notBetween",
16
- isEmpty = "isEmpty",
17
- notEmpty = "notEmpty",
18
- exists = "exists",
19
- notExists = "notExists",
20
- startsWith = "startsWith",
21
- endsWith = "endsWith"
22
- }
23
- declare enum ArrayOperator {
24
- all = "all",
25
- any = "any",
26
- none = "none",
27
- atLeast = "atLeast",
28
- atMost = "atMost",
29
- exactly = "exactly",
30
- empty = "empty",
31
- notEmpty = "notEmpty"
32
- }
33
- declare enum DateOperator {
34
- before = "before",
35
- after = "after",
36
- onOrBefore = "onOrBefore",
37
- onOrAfter = "onOrAfter",
38
- between = "between",
39
- notBetween = "notBetween",
40
- dayIn = "dayIn",// e.g., ['monday', 'tuesday', 'friday']
41
- dayNotIn = "dayNotIn"
42
- }
1
+ declare const Operator: {
2
+ readonly equals: "equals";
3
+ readonly notEquals: "notEquals";
4
+ readonly lessThan: "lessThan";
5
+ readonly lessThanEquals: "lessThanEquals";
6
+ readonly greaterThan: "greaterThan";
7
+ readonly greaterThanEquals: "greaterThanEquals";
8
+ readonly contains: "contains";
9
+ readonly notContains: "notContains";
10
+ readonly in: "in";
11
+ readonly notIn: "notIn";
12
+ readonly matches: "matches";
13
+ readonly notMatches: "notMatches";
14
+ readonly between: "between";
15
+ readonly notBetween: "notBetween";
16
+ readonly isEmpty: "isEmpty";
17
+ readonly notEmpty: "notEmpty";
18
+ readonly exists: "exists";
19
+ readonly notExists: "notExists";
20
+ readonly startsWith: "startsWith";
21
+ readonly endsWith: "endsWith";
22
+ };
23
+ type Operator = (typeof Operator)[keyof typeof Operator];
24
+ declare const ArrayOperator: {
25
+ readonly all: "all";
26
+ readonly any: "any";
27
+ readonly none: "none";
28
+ readonly atLeast: "atLeast";
29
+ readonly atMost: "atMost";
30
+ readonly exactly: "exactly";
31
+ readonly empty: "empty";
32
+ readonly notEmpty: "notEmpty";
33
+ };
34
+ type ArrayOperator = (typeof ArrayOperator)[keyof typeof ArrayOperator];
35
+ declare const DateOperator: {
36
+ readonly before: "before";
37
+ readonly after: "after";
38
+ readonly onOrBefore: "onOrBefore";
39
+ readonly onOrAfter: "onOrAfter";
40
+ readonly between: "between";
41
+ readonly notBetween: "notBetween";
42
+ readonly dayIn: "dayIn";
43
+ readonly dayNotIn: "dayNotIn";
44
+ };
45
+ type DateOperator = (typeof DateOperator)[keyof typeof DateOperator];
43
46
 
44
- type Rule = {
47
+ type OperatorValues = typeof Operator;
48
+ type ArrayOperatorValues = typeof ArrayOperator;
49
+ type DateOperatorValues = typeof DateOperator;
50
+ type RuleScalar = string | number | boolean | null | undefined;
51
+ type RuleValue = RuleScalar | Date | RegExp | RuleValue[] | {
52
+ [key: string]: RuleValue;
53
+ };
54
+ type OrderedRuleValue = string | number | Date;
55
+ type DateInputValue = string | number | Date;
56
+ type DateRuleValue = DateInputValue | [DateInputValue, DateInputValue] | string[];
57
+ type ValueSource<TValue> = {
58
+ value: TValue;
59
+ path?: never;
60
+ } | {
61
+ path: string;
62
+ value?: never;
63
+ };
64
+ type NoValueSource = {
65
+ value?: never;
66
+ path?: never;
67
+ };
68
+ type RuleBase<TOperator extends Operator> = {
69
+ field: string;
70
+ operator: TOperator;
71
+ error?: string;
72
+ };
73
+ type DateRuleBase<TOperator extends DateOperator> = {
74
+ field: string;
75
+ dateOperator: TOperator;
76
+ error?: string;
77
+ };
78
+ type StrictEqualityRule<TValue = RuleValue> = (RuleBase<OperatorValues['equals']> & ValueSource<TValue>) | (RuleBase<OperatorValues['notEquals']> & ValueSource<TValue>);
79
+ type StrictOrderedComparisonRule = (RuleBase<OperatorValues['lessThan']> & ValueSource<OrderedRuleValue>) | (RuleBase<OperatorValues['lessThanEquals']> & ValueSource<OrderedRuleValue>) | (RuleBase<OperatorValues['greaterThan']> & ValueSource<OrderedRuleValue>) | (RuleBase<OperatorValues['greaterThanEquals']> & ValueSource<OrderedRuleValue>);
80
+ type StrictMembershipRule<TValue = RuleValue> = (RuleBase<OperatorValues['in']> & ValueSource<TValue[]>) | (RuleBase<OperatorValues['notIn']> & ValueSource<TValue[]>);
81
+ type StrictContainsRule<TValue = RuleValue> = (RuleBase<OperatorValues['contains']> & ValueSource<TValue>) | (RuleBase<OperatorValues['notContains']> & ValueSource<TValue>);
82
+ type StrictPatternRule = (RuleBase<OperatorValues['matches']> & ValueSource<RegExp | string>) | (RuleBase<OperatorValues['notMatches']> & ValueSource<RegExp | string>);
83
+ type StrictStringBoundaryRule = (RuleBase<OperatorValues['startsWith']> & ValueSource<string>) | (RuleBase<OperatorValues['endsWith']> & ValueSource<string>);
84
+ type StrictRangeRule = (RuleBase<OperatorValues['between']> & ValueSource<[OrderedRuleValue, OrderedRuleValue]>) | (RuleBase<OperatorValues['notBetween']> & ValueSource<[OrderedRuleValue, OrderedRuleValue]>);
85
+ type StrictPresenceRule = (RuleBase<OperatorValues['isEmpty']> & NoValueSource) | (RuleBase<OperatorValues['notEmpty']> & NoValueSource) | (RuleBase<OperatorValues['exists']> & NoValueSource) | (RuleBase<OperatorValues['notExists']> & NoValueSource);
86
+ type StrictRule<TValue = RuleValue> = StrictEqualityRule<TValue> | StrictOrderedComparisonRule | StrictMembershipRule<TValue> | StrictContainsRule<TValue> | StrictPatternRule | StrictStringBoundaryRule | StrictRangeRule | StrictPresenceRule;
87
+ type ArrayType = 'jsonb' | 'native';
88
+ type ArrayRuleBase<TOperator extends ArrayOperator> = {
89
+ field: string;
90
+ arrayOperator: TOperator;
91
+ arrayType?: ArrayType;
92
+ error?: string;
93
+ };
94
+ type StrictArrayPredicateRule<TRuleValue = RuleValue, TDateValue = DateRuleValue> = (ArrayRuleBase<ArrayOperatorValues['all']> & {
95
+ condition: StrictCondition<TRuleValue, TDateValue>;
96
+ count?: never;
97
+ }) | (ArrayRuleBase<ArrayOperatorValues['any']> & {
98
+ condition: StrictCondition<TRuleValue, TDateValue>;
99
+ count?: never;
100
+ }) | (ArrayRuleBase<ArrayOperatorValues['none']> & {
101
+ condition: StrictCondition<TRuleValue, TDateValue>;
102
+ count?: never;
103
+ });
104
+ type StrictArrayCountRule<TRuleValue = RuleValue, TDateValue = DateRuleValue> = (ArrayRuleBase<ArrayOperatorValues['atLeast']> & {
105
+ condition?: StrictCondition<TRuleValue, TDateValue>;
106
+ count?: number;
107
+ }) | (ArrayRuleBase<ArrayOperatorValues['atMost']> & {
108
+ condition?: StrictCondition<TRuleValue, TDateValue>;
109
+ count?: number;
110
+ }) | (ArrayRuleBase<ArrayOperatorValues['exactly']> & {
111
+ condition?: StrictCondition<TRuleValue, TDateValue>;
112
+ count?: number;
113
+ });
114
+ type StrictArrayPresenceRule = (ArrayRuleBase<ArrayOperatorValues['empty']> & {
115
+ condition?: never;
116
+ count?: never;
117
+ }) | (ArrayRuleBase<ArrayOperatorValues['notEmpty']> & {
118
+ condition?: never;
119
+ count?: never;
120
+ });
121
+ type StrictArrayRule<TRuleValue = RuleValue, TDateValue = DateRuleValue> = StrictArrayPredicateRule<TRuleValue, TDateValue> | StrictArrayCountRule<TRuleValue, TDateValue> | StrictArrayPresenceRule;
122
+ type StrictDateComparisonRule = (DateRuleBase<DateOperatorValues['before']> & ValueSource<DateInputValue>) | (DateRuleBase<DateOperatorValues['after']> & ValueSource<DateInputValue>) | (DateRuleBase<DateOperatorValues['onOrBefore']> & ValueSource<DateInputValue>) | (DateRuleBase<DateOperatorValues['onOrAfter']> & ValueSource<DateInputValue>);
123
+ type StrictDateRangeRule = (DateRuleBase<DateOperatorValues['between']> & ValueSource<[DateInputValue, DateInputValue]>) | (DateRuleBase<DateOperatorValues['notBetween']> & ValueSource<[DateInputValue, DateInputValue]>);
124
+ type StrictDateDayRule = (DateRuleBase<DateOperatorValues['dayIn']> & {
125
+ value: string[];
126
+ path?: never;
127
+ }) | (DateRuleBase<DateOperatorValues['dayNotIn']> & {
128
+ value: string[];
129
+ path?: never;
130
+ });
131
+ type StrictDateRule = StrictDateComparisonRule | StrictDateRangeRule | StrictDateDayRule;
132
+ type Rule<TValue = RuleValue> = {
45
133
  field: string;
46
134
  operator: Operator;
47
- value?: any;
135
+ value?: TValue;
48
136
  path?: string;
49
137
  error?: string;
50
138
  };
51
- type ArrayType = 'jsonb' | 'native';
52
- type ArrayRule = {
139
+ type ArrayRule<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
53
140
  field: string;
54
141
  arrayOperator: ArrayOperator;
55
142
  arrayType?: ArrayType;
56
- condition?: Condition;
143
+ condition?: Condition<TRuleValue, TDateValue>;
57
144
  count?: number;
58
145
  error?: string;
59
146
  };
60
- type DateRule = {
147
+ type DateRule<TValue = DateRuleValue> = {
61
148
  field: string;
62
149
  dateOperator: DateOperator;
63
- value?: any;
150
+ value?: TValue;
64
151
  path?: string;
65
152
  error?: string;
66
153
  };
67
- type All = {
68
- all: Condition[];
154
+ type All<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
155
+ all: Condition<TRuleValue, TDateValue>[];
156
+ error?: string;
157
+ };
158
+ type Any<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
159
+ any: Condition<TRuleValue, TDateValue>[];
160
+ error?: string;
161
+ };
162
+ type IfThenElse<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
163
+ if: Condition<TRuleValue, TDateValue>;
164
+ then: Condition<TRuleValue, TDateValue>;
165
+ else?: Condition<TRuleValue, TDateValue>;
166
+ error?: string;
167
+ };
168
+ type Condition<TRuleValue = RuleValue, TDateValue = DateRuleValue> = Rule<TRuleValue> | ArrayRule<TRuleValue, TDateValue> | DateRule<TDateValue> | All<TRuleValue, TDateValue> | Any<TRuleValue, TDateValue> | IfThenElse<TRuleValue, TDateValue> | boolean;
169
+ type StrictAll<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
170
+ all: StrictCondition<TRuleValue, TDateValue>[];
69
171
  error?: string;
70
172
  };
71
- type Any = {
72
- any: Condition[];
173
+ type StrictAny<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
174
+ any: StrictCondition<TRuleValue, TDateValue>[];
73
175
  error?: string;
74
176
  };
75
- type IfThenElse = {
76
- if: Condition;
77
- then: Condition;
78
- else?: Condition;
177
+ type StrictIfThenElse<TRuleValue = RuleValue, TDateValue = DateRuleValue> = {
178
+ if: StrictCondition<TRuleValue, TDateValue>;
179
+ then: StrictCondition<TRuleValue, TDateValue>;
180
+ else?: StrictCondition<TRuleValue, TDateValue>;
79
181
  error?: string;
80
182
  };
81
- type Condition = Rule | ArrayRule | DateRule | All | Any | IfThenElse | boolean;
183
+ type StrictCondition<TRuleValue = RuleValue, TDateValue = DateRuleValue> = StrictRule<TRuleValue> | StrictArrayRule<TRuleValue, TDateValue> | StrictDateRule | StrictAll<TRuleValue, TDateValue> | StrictAny<TRuleValue, TDateValue> | StrictIfThenElse<TRuleValue, TDateValue> | boolean;
184
+
185
+ declare const check: <TData extends Record<string, unknown>>(conditions: Condition, data: TData, context?: TData) => boolean | string;
186
+
187
+ type PrismaWhere = Record<string, unknown>;
188
+ type FieldMapEntry = {
189
+ kind: 'scalar' | 'object' | 'enum';
190
+ type: string;
191
+ isList?: boolean;
192
+ fromFields?: string[];
193
+ toFields?: string[];
194
+ relationName?: string;
195
+ };
196
+ interface FieldMap {
197
+ [modelName: string]: {
198
+ dbName?: string | null;
199
+ fields: {
200
+ [fieldName: string]: FieldMapEntry;
201
+ };
202
+ };
203
+ }
204
+ type StepRef = {
205
+ __step: number;
206
+ };
207
+ type GroupByStep = {
208
+ operation: 'groupBy';
209
+ model: string;
210
+ args: {
211
+ by: string[];
212
+ where: Record<string, unknown>;
213
+ having: Record<string, unknown>;
214
+ };
215
+ extract: string;
216
+ };
217
+ type WhereStep = {
218
+ operation: 'where';
219
+ where: Record<string, unknown>;
220
+ };
221
+ type PrismaStep = GroupByStep | WhereStep;
222
+ type ToPrismaResult = {
223
+ steps: PrismaStep[];
224
+ };
225
+ type BuildOptions = {
226
+ map?: FieldMap;
227
+ model?: string;
228
+ context?: Record<string, unknown>;
229
+ };
82
230
 
83
- declare const check: (conditions: Condition, data: any, context?: any) => boolean | string;
231
+ /**
232
+ * Execute a Prisma query plan produced by toPrisma().
233
+ *
234
+ * The plan is a flat list of steps where all but the last are `groupBy` steps
235
+ * that feed results (via { __step: N } sentinels) into subsequent steps.
236
+ * The final step is always a `where` step whose resolved WHERE clause is returned.
237
+ *
238
+ * @param result - Result from toPrisma()
239
+ * @param prismaDelegate - Map of camelCase model name → Prisma delegate
240
+ * e.g. { post: prisma.post, user: prisma.user }
241
+ * @returns The resolved WHERE clause (ready for findMany/count/etc.)
242
+ *
243
+ * @example
244
+ * const plan = toPrisma(condition, { map, model: 'User' });
245
+ * const where = await executePrismaQueryPlan(plan, { post: prisma.post });
246
+ * await prisma.user.findMany({ where });
247
+ */
248
+ declare const executePrismaQueryPlan: (result: ToPrismaResult, prismaDelegate: Record<string, Record<string, (...args: unknown[]) => unknown>>) => Promise<Record<string, unknown>>;
249
+
250
+ /**
251
+ * Convert a json-rules Condition to a Prisma query plan.
252
+ *
253
+ * Returns a `ToPrismaResult` with:
254
+ * - `where` – the Prisma WHERE clause
255
+ * - `steps` – optional array of groupBy steps for count-based relation filters
256
+ * (only present when `atLeast`/`atMost`/`exactly` operators are used with a map)
257
+ *
258
+ * When `steps` is present, pass the result to `executePrismaQueryPlan` to
259
+ * resolve step refs before using `where` in a Prisma query.
260
+ *
261
+ * @param condition - The rule condition to convert
262
+ * @param options - Optional map, model, and context
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * // Simple scalar
267
+ * toPrisma({ field: 'status', operator: Operator.equals, value: 'active' })
268
+ * // → { where: { status: { equals: 'active' } } }
269
+ *
270
+ * // JSON field detection (map required)
271
+ * toPrisma({ field: 'metadata.theme', operator: Operator.equals, value: 'dark' }, { map, model: 'User' })
272
+ * // → { where: { metadata: { path: ['theme'], equals: 'dark' } } }
273
+ *
274
+ * // Context path ref
275
+ * toPrisma({ field: 'userId', operator: Operator.equals, path: 'currentUser.id' }, { context: { currentUser: { id: '123' } } })
276
+ * // → { where: { userId: { equals: '123' } } }
277
+ *
278
+ * // Multi-step (map required)
279
+ * const plan = toPrisma({ field: 'posts', arrayOperator: 'atLeast', count: 3, condition: {...} }, { map, model: 'User' });
280
+ * const where = await executePrismaQueryPlan(plan, { post: prisma.post });
281
+ * await prisma.user.findMany({ where });
282
+ * ```
283
+ */
284
+ declare const toPrisma: (condition: Condition, options?: BuildOptions) => ToPrismaResult;
84
285
 
85
286
  type SqlResult = {
86
287
  sql: string;
87
288
  params: unknown[];
289
+ joins: string[];
88
290
  };
89
291
 
292
+ type SqlBuildOptions = {
293
+ map?: FieldMap;
294
+ model?: string;
295
+ alias?: string;
296
+ context?: Record<string, unknown>;
297
+ };
90
298
  /**
91
299
  * Convert a json-rules Condition to a PostgreSQL WHERE clause.
92
300
  *
93
301
  * @param condition - The rule condition to convert
94
- * @returns Object with `sql` (WHERE clause fragment) and `params` array
302
+ * @param options - Optional map/model/alias for JOIN generation; context for path refs
303
+ * @returns Object with `sql`, `params`, and `joins` (LEFT JOIN clauses)
95
304
  *
96
305
  * @example
97
306
  * ```typescript
98
- * import { toSql, Operator } from '@inixiative/json-rules';
99
- *
100
- * const rule = { field: 'status', operator: Operator.equals, value: 'active' };
101
- * const { sql, params } = toSql(rule);
307
+ * // Simple field
308
+ * const { sql, params } = toSql({ field: 'status', operator: Operator.equals, value: 'active' });
102
309
  * // sql: '"status" = $1'
103
- * // params: ['active']
104
310
  *
105
- * // Complex rule
106
- * const rule2 = {
107
- * all: [
108
- * { field: 'deletedAt', operator: Operator.equals, value: null },
109
- * { field: 'status', operator: Operator.in, value: ['active', 'pending'] }
110
- * ]
111
- * };
112
- * const { sql: sql2, params: params2 } = toSql(rule2);
113
- * // sql: '("deletedAt" IS NULL AND "status" = ANY($1))'
114
- * // params: [['active', 'pending']]
311
+ * // Relation traversal with JOINs (map required)
312
+ * const { sql, params, joins } = toSql(
313
+ * { field: 'author.email', operator: Operator.equals, value: 'a@b.com' },
314
+ * { map, model: 'Post', alias: 't0' }
315
+ * );
316
+ * // sql: '"t1"."email" = $1'
317
+ * // joins: ['LEFT JOIN "User" AS "t1" ON "t1"."id" = "t0"."authorId"']
318
+ *
319
+ * // Same-record field comparison ($.field)
320
+ * const { sql: sql2 } = toSql({ field: 'endDate', operator: Operator.greaterThan, path: '$.startDate' });
321
+ * // sql2: '"endDate" > "startDate"'
322
+ *
323
+ * // External context ref
324
+ * const { sql: sql3 } = toSql(
325
+ * { field: 'userId', operator: Operator.equals, path: 'currentUser.id' },
326
+ * { context: { currentUser: { id: '123' } } }
327
+ * );
328
+ * // sql3: '"userId" = $1' params: ['123']
115
329
  * ```
116
330
  */
117
- declare const toSql: (condition: Condition) => SqlResult;
331
+ declare const toSql: (condition: Condition, options?: SqlBuildOptions) => SqlResult;
332
+
333
+ type RuleValidationTarget = 'check' | 'toPrisma' | 'toSql';
334
+ type ValidationIssue = {
335
+ path: string;
336
+ message: string;
337
+ code: string;
338
+ };
339
+ type ValidationResult = {
340
+ ok: boolean;
341
+ errors: ValidationIssue[];
342
+ };
343
+ declare const validateRule: (condition: unknown, options?: {
344
+ target?: RuleValidationTarget;
345
+ }) => ValidationResult;
346
+ declare const assertValidRule: (condition: unknown, options?: {
347
+ target?: RuleValidationTarget;
348
+ }) => asserts condition is Condition;
118
349
 
119
- export { type All, type Any, ArrayOperator, type ArrayRule, type ArrayType, type Condition, DateOperator, type DateRule, type IfThenElse, Operator, type Rule, type SqlResult, check, toSql };
350
+ export { type All, type Any, ArrayOperator, type ArrayRule, type ArrayType, type BuildOptions, type Condition, type DateInputValue, DateOperator, type DateRule, type DateRuleValue, type FieldMap, type FieldMapEntry, type GroupByStep, type IfThenElse, Operator, type OrderedRuleValue, type PrismaStep, type PrismaWhere, type Rule, type RuleScalar, type RuleValidationTarget, type RuleValue, type SqlResult, type StepRef, type StrictAll, type StrictAny, type StrictArrayCountRule, type StrictArrayPredicateRule, type StrictArrayPresenceRule, type StrictArrayRule, type StrictCondition, type StrictContainsRule, type StrictDateComparisonRule, type StrictDateDayRule, type StrictDateRangeRule, type StrictDateRule, type StrictEqualityRule, type StrictIfThenElse, type StrictMembershipRule, type StrictOrderedComparisonRule, type StrictPatternRule, type StrictPresenceRule, type StrictRangeRule, type StrictRule, type StrictStringBoundaryRule, type ToPrismaResult, type ValidationIssue, type ValidationResult, type WhereStep, assertValidRule, check, executePrismaQueryPlan, toPrisma, toSql, validateRule };