@fjell/core 4.4.64 → 4.4.65

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/README.md CHANGED
@@ -134,10 +134,51 @@ import {
134
134
  validateLocations,
135
135
  validateKey,
136
136
  validatePK,
137
- validateKeys
137
+ validateKeys,
138
+ validateSchema,
139
+ SchemaValidator
138
140
  } from '@fjell/core/validation';
139
141
  ```
140
142
 
143
+ #### Schema Validation (Zod, Yup, etc.)
144
+
145
+ Universal schema validation that works with any validator matching the `SchemaValidator` interface. Zod is supported out of the box via duck typing:
146
+
147
+ ```typescript
148
+ import { validateSchema } from '@fjell/core/validation';
149
+ import { z } from 'zod';
150
+
151
+ // Define a Zod schema
152
+ const userSchema = z.object({
153
+ name: z.string().min(3),
154
+ age: z.number().min(18),
155
+ email: z.string().email().optional()
156
+ });
157
+
158
+ // Validate data before caching, sending to API, or persisting
159
+ const validatedUser = await validateSchema(userData, userSchema);
160
+
161
+ // Validation throws ValidationError with FieldError[] on failure
162
+ try {
163
+ await validateSchema({ name: 'Al', age: 10 }, userSchema);
164
+ } catch (error) {
165
+ if (error instanceof ValidationError) {
166
+ console.log(error.fieldErrors);
167
+ // [
168
+ // { path: ['name'], message: 'String must contain at least 3 character(s)', code: 'too_small' },
169
+ // { path: ['age'], message: 'Number must be greater than or equal to 18', code: 'too_small' }
170
+ // ]
171
+ }
172
+ }
173
+ ```
174
+
175
+ **Schema Validation Features:**
176
+ - **Universal**: Works with Zod, Yup, Joi, or any validator matching the interface
177
+ - **Type-Safe**: Full TypeScript support with type inference
178
+ - **Error Transformation**: Automatically converts validator errors to Fjell `ValidationError`
179
+ - **Async Support**: Handles both sync and async validation
180
+ - **Available Everywhere**: Use in cache layers, APIs, persistence, or any system boundary
181
+
141
182
  #### Location Validation
142
183
 
143
184
  Validates that location key arrays match the expected coordinate hierarchy:
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export * from './validation';
15
15
  export * from './errors';
16
16
  export * from './operations';
17
17
  export * from './event';
18
- export type { Operations, OperationParams, AffectedKeys, CreateOptions, UpdateOptions, AllOptions, PaginationMetadata, AllOperationResult } from './operations/Operations';
18
+ export type { Operations, OperationParams, AffectedKeys, CreateOptions, UpdateOptions, PaginationOptions, AllOptions, FindOptions, PaginationMetadata, OperationResult, AllOperationResult, FindOperationResult } from './operations/Operations';
19
19
  export { isPriKey as isOperationPriKey, isComKey as isOperationComKey } from './operations/Operations';
20
20
  export type { GetMethod, CreateMethod, UpdateMethod, RemoveMethod, UpsertMethod, AllMethod, OneMethod, FindMethod, FindOneMethod, FinderMethod, ActionMethod, ActionOperationMethod, AllActionMethod, AllActionOperationMethod, FacetMethod, FacetOperationMethod, AllFacetMethod, AllFacetOperationMethod, OperationsExtensions } from './operations/methods';
21
21
  export * from './operations/wrappers';
package/dist/index.js CHANGED
@@ -1438,6 +1438,47 @@ var DuplicateError = class extends ActionError {
1438
1438
  }
1439
1439
  };
1440
1440
 
1441
+ // src/validation/schema.ts
1442
+ async function validateSchema(data, schema) {
1443
+ if (!schema) {
1444
+ return data;
1445
+ }
1446
+ try {
1447
+ if (schema.parseAsync) {
1448
+ return await schema.parseAsync(data);
1449
+ }
1450
+ const result = schema.safeParse(data);
1451
+ if (result.success) {
1452
+ return result.data;
1453
+ } else {
1454
+ throw result.error;
1455
+ }
1456
+ } catch (error) {
1457
+ if (error && Array.isArray(error.issues)) {
1458
+ const fieldErrors = error.issues.map((issue) => ({
1459
+ path: issue.path,
1460
+ message: issue.message,
1461
+ code: issue.code
1462
+ }));
1463
+ const validationError = new ValidationError(
1464
+ "Schema validation failed",
1465
+ // eslint-disable-next-line no-undefined
1466
+ void 0,
1467
+ // eslint-disable-next-line no-undefined
1468
+ void 0,
1469
+ // eslint-disable-next-line no-undefined
1470
+ void 0,
1471
+ fieldErrors
1472
+ );
1473
+ throw validationError;
1474
+ }
1475
+ if (error instanceof ValidationError) {
1476
+ throw error;
1477
+ }
1478
+ throw new ValidationError(`Validation failed: ${error.message || "Unknown error"}`);
1479
+ }
1480
+ }
1481
+
1441
1482
  // src/operations/errorEnhancer.ts
1442
1483
  async function executeWithContext(operation, context) {
1443
1484
  try {
@@ -2096,11 +2137,38 @@ function createRemoveWrapper(coordinate, implementation, options = {}) {
2096
2137
 
2097
2138
  // src/operations/wrappers/createFindWrapper.ts
2098
2139
  var logger16 = logger_default.get("operations", "wrappers", "find");
2140
+ function isFindOperationResult(value) {
2141
+ return value && typeof value === "object" && "items" in value && "metadata" in value && Array.isArray(value.items) && value.metadata && typeof value.metadata === "object" && "total" in value.metadata;
2142
+ }
2143
+ function applyPagination(items, options) {
2144
+ const total = items.length;
2145
+ const offset = options?.offset ?? 0;
2146
+ const limit = options?.limit;
2147
+ let paginatedItems = items;
2148
+ if (offset > 0) {
2149
+ paginatedItems = paginatedItems.slice(offset);
2150
+ }
2151
+ if (limit != null && limit >= 0) {
2152
+ paginatedItems = paginatedItems.slice(0, limit);
2153
+ }
2154
+ const returned = paginatedItems.length;
2155
+ const hasMore = limit != null && offset + returned < total;
2156
+ return {
2157
+ items: paginatedItems,
2158
+ metadata: {
2159
+ total,
2160
+ returned,
2161
+ offset,
2162
+ limit,
2163
+ hasMore
2164
+ }
2165
+ };
2166
+ }
2099
2167
  function createFindWrapper(coordinate, implementation, options = {}) {
2100
2168
  const operationName = options.operationName || "find";
2101
- return async (finder, params, locations) => {
2169
+ return async (finder, params, locations, findOptions) => {
2102
2170
  if (options.debug) {
2103
- logger16.debug(`[${operationName}] Called:`, { finder, params, locations });
2171
+ logger16.debug(`[${operationName}] Called:`, { finder, params, locations, findOptions });
2104
2172
  }
2105
2173
  if (!options.skipValidation) {
2106
2174
  validateFinderName(finder, operationName);
@@ -2110,19 +2178,36 @@ function createFindWrapper(coordinate, implementation, options = {}) {
2110
2178
  const normalizedParams = params ?? {};
2111
2179
  const normalizedLocations = locations ?? [];
2112
2180
  try {
2113
- const result = await implementation(finder, normalizedParams, normalizedLocations);
2114
- if (options.debug) {
2115
- logger16.debug(`[${operationName}] Found ${result.length} items for finder "${finder}"`);
2116
- }
2117
- if (!options.skipValidation) {
2118
- return validatePK(result, coordinate.kta[0]);
2181
+ const result = await implementation(finder, normalizedParams, normalizedLocations, findOptions);
2182
+ if (isFindOperationResult(result)) {
2183
+ if (options.debug) {
2184
+ logger16.debug(`[${operationName}] Finder "${finder}" opted-in to pagination, found ${result.items.length} items (total: ${result.metadata.total})`);
2185
+ }
2186
+ if (!options.skipValidation) {
2187
+ const validatedItems = validatePK(result.items, coordinate.kta[0]);
2188
+ return {
2189
+ items: validatedItems,
2190
+ metadata: result.metadata
2191
+ };
2192
+ }
2193
+ return result;
2194
+ } else {
2195
+ if (options.debug) {
2196
+ logger16.debug(`[${operationName}] Finder "${finder}" using legacy signature, applying post-processing pagination`);
2197
+ }
2198
+ let validatedItems;
2199
+ if (!options.skipValidation) {
2200
+ validatedItems = validatePK(result, coordinate.kta[0]);
2201
+ } else {
2202
+ validatedItems = result;
2203
+ }
2204
+ return applyPagination(validatedItems, findOptions);
2119
2205
  }
2120
- return result;
2121
2206
  } catch (error) {
2122
2207
  if (options.onError) {
2123
2208
  const context = {
2124
2209
  operationName,
2125
- params: [finder, params, locations],
2210
+ params: [finder, params, locations, findOptions],
2126
2211
  coordinate
2127
2212
  };
2128
2213
  throw options.onError(error, context);
@@ -2454,5 +2539,6 @@ export {
2454
2539
  validateOperationParams,
2455
2540
  validatePK,
2456
2541
  validatePriKey,
2457
- validateQuery
2542
+ validateQuery,
2543
+ validateSchema
2458
2544
  };
@@ -20,23 +20,13 @@ export type CreateOptions<S extends string, L1 extends string = never, L2 extend
20
20
  locations: LocKeyArray<L1, L2, L3, L4, L5>;
21
21
  };
22
22
  /**
23
- * Options for the all() operation to control pagination.
23
+ * Base options for pagination operations (all() and find()).
24
24
  *
25
- * When provided, these options take precedence over any limit/offset
26
- * specified in the ItemQuery.
25
+ * Contains the common pagination parameters shared by both operations.
27
26
  *
28
27
  * @public
29
- *
30
- * @example
31
- * ```typescript
32
- * // Fetch first 50 items
33
- * const result = await operations.all(query, [], { limit: 50, offset: 0 });
34
- *
35
- * // Fetch next page
36
- * const nextPage = await operations.all(query, [], { limit: 50, offset: 50 });
37
- * ```
38
28
  */
39
- export interface AllOptions {
29
+ export interface PaginationOptions {
40
30
  /**
41
31
  * Maximum number of items to return.
42
32
  *
@@ -54,6 +44,41 @@ export interface AllOptions {
54
44
  */
55
45
  offset?: number;
56
46
  }
47
+ /**
48
+ * Options for the all() operation with pagination support.
49
+ *
50
+ * When provided, these options take precedence over any limit/offset
51
+ * specified in the ItemQuery.
52
+ *
53
+ * @public
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Fetch first 50 items
58
+ * const result = await operations.all(query, [], { limit: 50, offset: 0 });
59
+ *
60
+ * // Fetch next page
61
+ * const nextPage = await operations.all(query, [], { limit: 50, offset: 50 });
62
+ * ```
63
+ */
64
+ export interface AllOptions extends PaginationOptions {
65
+ }
66
+ /**
67
+ * Options for the find() operation with pagination support.
68
+ *
69
+ * When provided, these options take precedence over any limit/offset
70
+ * passed to finder functions.
71
+ *
72
+ * @public
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * // Find with pagination
77
+ * const findResult = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10 });
78
+ * ```
79
+ */
80
+ export interface FindOptions extends PaginationOptions {
81
+ }
57
82
  /**
58
83
  * Metadata about the pagination state for an all() operation.
59
84
  *
@@ -105,6 +130,26 @@ export interface PaginationMetadata {
105
130
  */
106
131
  hasMore: boolean;
107
132
  }
133
+ /**
134
+ * Base result structure for paginated operations.
135
+ *
136
+ * This structure provides both the items and metadata needed for
137
+ * implementing proper pagination in applications.
138
+ *
139
+ * @template T - The item type being returned
140
+ *
141
+ * @public
142
+ */
143
+ export interface OperationResult<T> {
144
+ /**
145
+ * Array of items matching the query, with limit/offset applied.
146
+ */
147
+ items: T[];
148
+ /**
149
+ * Pagination metadata for the result set.
150
+ */
151
+ metadata: PaginationMetadata;
152
+ }
108
153
  /**
109
154
  * Result structure for the all() operation with pagination support.
110
155
  *
@@ -127,15 +172,31 @@ export interface PaginationMetadata {
127
172
  * }
128
173
  * ```
129
174
  */
130
- export interface AllOperationResult<T> {
131
- /**
132
- * Array of items matching the query, with limit/offset applied.
133
- */
134
- items: T[];
135
- /**
136
- * Pagination metadata for the result set.
137
- */
138
- metadata: PaginationMetadata;
175
+ export interface AllOperationResult<T> extends OperationResult<T> {
176
+ }
177
+ /**
178
+ * Result structure for the find() operation with pagination support.
179
+ *
180
+ * This structure provides both the items and metadata needed for
181
+ * implementing proper pagination in find operations.
182
+ *
183
+ * @template T - The item type being returned
184
+ *
185
+ * @public
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const result = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10 });
190
+ *
191
+ * console.log(`Found ${result.metadata.returned} of ${result.metadata.total} results`);
192
+ * // "Found 10 of 25 results"
193
+ *
194
+ * if (result.metadata.hasMore) {
195
+ * // Load next page
196
+ * }
197
+ * ```
198
+ */
199
+ export interface FindOperationResult<T> extends OperationResult<T> {
139
200
  }
140
201
  /**
141
202
  * Options for update operations across all Fjell libraries.
@@ -340,27 +401,36 @@ export interface Operations<V extends Item<S, L1, L2, L3, L4, L5>, S extends str
340
401
  */
341
402
  remove(key: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>): Promise<V | void>;
342
403
  /**
343
- * Executes a finder method by name.
404
+ * Executes a finder method by name with optional pagination support.
405
+ *
406
+ * Supports hybrid pagination approach:
407
+ * - If finder returns FindOperationResult<V>, uses it directly (opt-in)
408
+ * - If finder returns V[], framework applies post-processing pagination
344
409
  *
345
410
  * @param finder - Name of the finder method
346
411
  * @param params - Parameters for the finder
347
412
  * @param locations - Optional location hierarchy to scope the query
348
- * @returns Array of items found
413
+ * @param options - Optional pagination options (limit, offset)
414
+ * @returns Result containing items and pagination metadata
349
415
  *
350
416
  * @example
351
417
  * ```typescript
352
- * // Find users by email
353
- * const users = await operations.find('byEmail', { email: 'alice@example.com' });
418
+ * // Find users by email (no pagination)
419
+ * const result = await operations.find('byEmail', { email: 'alice@example.com' });
420
+ * const users = result.items;
354
421
  *
355
- * // Find in specific location
356
- * const comments = await operations.find(
422
+ * // Find with pagination
423
+ * const result = await operations.find(
357
424
  * 'byAuthor',
358
425
  * { author: 'alice' },
359
- * [{kt: 'post', lk: 'post-123'}]
426
+ * [{kt: 'post', lk: 'post-123'}],
427
+ * { limit: 10, offset: 0 }
360
428
  * );
429
+ * const comments = result.items;
430
+ * const total = result.metadata.total;
361
431
  * ```
362
432
  */
363
- find(finder: string, params?: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
433
+ find(finder: string, params?: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
364
434
  /**
365
435
  * Executes a finder method and returns the first result.
366
436
  *
@@ -1,7 +1,7 @@
1
1
  import { Item } from "../items";
2
2
  import { ComKey, LocKeyArray } from "../keys";
3
3
  import { ItemQuery } from "../item/ItemQuery";
4
- import { AffectedKeys, AllOperationResult, AllOptions, OperationParams, Operations } from "./Operations";
4
+ import { AffectedKeys, AllOperationResult, AllOptions, FindOperationResult, FindOptions, OperationParams, Operations } from "./Operations";
5
5
  /**
6
6
  * Contained Operations interface - specialized for contained (hierarchical) items only.
7
7
  *
@@ -49,7 +49,7 @@ import { AffectedKeys, AllOperationResult, AllOptions, OperationParams, Operatio
49
49
  export interface ContainedOperations<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> extends Omit<Operations<V, S, L1, L2, L3, L4, L5>, 'get' | 'update' | 'remove' | 'upsert' | 'create' | 'action' | 'facet' | 'allAction' | 'allFacet' | 'all' | 'one' | 'find' | 'findOne'> {
50
50
  all(query: ItemQuery | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | [], allOptions?: AllOptions): Promise<AllOperationResult<V>>;
51
51
  one(query: ItemQuery | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
52
- find(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
52
+ find(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
53
53
  findOne(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
54
54
  create(item: Partial<Item<S, L1, L2, L3, L4, L5>>, options?: {
55
55
  locations?: LocKeyArray<L1, L2, L3, L4, L5>;
@@ -1,7 +1,7 @@
1
1
  import { Item } from "../items";
2
2
  import { ComKey, LocKeyArray, PriKey } from "../keys";
3
3
  import { ItemQuery } from "../item/ItemQuery";
4
- import { AffectedKeys, AllOperationResult, AllOptions, CreateOptions, OperationParams, UpdateOptions } from "./Operations";
4
+ import { AffectedKeys, AllOperationResult, AllOptions, CreateOptions, FindOperationResult, FindOptions, OperationParams, UpdateOptions } from "./Operations";
5
5
  /**
6
6
  * Get method signature - retrieves single item by key
7
7
  */
@@ -69,10 +69,29 @@ export interface OneMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends stri
69
69
  (query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
70
70
  }
71
71
  /**
72
- * Find method signature - finds multiple items using finder
72
+ * Find method signature - finds multiple items using finder with optional pagination.
73
+ *
74
+ * Supports hybrid approach:
75
+ * - If finder returns FindOperationResult<V>, uses it directly (opt-in)
76
+ * - If finder returns V[], framework applies post-processing pagination
77
+ *
78
+ * @param finder - Name of the finder method
79
+ * @param params - Parameters for the finder
80
+ * @param locations - Optional location hierarchy to scope the query
81
+ * @param options - Optional pagination options (limit, offset)
82
+ * @returns Result containing items and pagination metadata
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // Without pagination (returns all results)
87
+ * const result = await operations.find('byEmail', { email: 'test@example.com' });
88
+ *
89
+ * // With pagination
90
+ * const result = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10, offset: 0 });
91
+ * ```
73
92
  */
74
93
  export interface FindMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
75
- (finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
94
+ (finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
76
95
  }
77
96
  /**
78
97
  * FindOne method signature - finds single item using finder
@@ -81,17 +100,44 @@ export interface FindOneMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends
81
100
  (finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
82
101
  }
83
102
  /**
84
- * Finder method signature - finds multiple items
103
+ * Finder method signature - finds multiple items.
85
104
  *
86
- * @example
105
+ * Supports hybrid approach for pagination:
106
+ * - **Legacy signature**: Return `Promise<V[]>` - framework applies post-processing pagination
107
+ * - **Opt-in signature**: Return `Promise<FindOperationResult<V>>` - finder handles pagination at source
108
+ *
109
+ * @example Legacy finder (framework handles pagination)
87
110
  * ```typescript
88
111
  * const byEmailFinder: FinderMethod<User, 'user'> = async (params) => {
89
112
  * return await database.findUsers({ email: params.email });
90
113
  * };
91
114
  * ```
115
+ *
116
+ * @example Opt-in finder (finder handles pagination)
117
+ * ```typescript
118
+ * const byEmailFinder: FinderMethod<User, 'user'> = async (params, locations, options) => {
119
+ * const query = buildQuery({ email: params.email });
120
+ * const total = await database.count(query);
121
+ *
122
+ * if (options?.offset) query.offset(options.offset);
123
+ * if (options?.limit) query.limit(options.limit);
124
+ *
125
+ * const items = await database.find(query);
126
+ * return {
127
+ * items,
128
+ * metadata: {
129
+ * total,
130
+ * returned: items.length,
131
+ * offset: options?.offset ?? 0,
132
+ * limit: options?.limit,
133
+ * hasMore: (options?.offset ?? 0) + items.length < total
134
+ * }
135
+ * };
136
+ * };
137
+ * ```
92
138
  */
93
139
  export interface FinderMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
94
- (params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
140
+ (params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<V[] | FindOperationResult<V>>;
95
141
  }
96
142
  /**
97
143
  * Action operation method signature - executes action on specific item by key
@@ -1,7 +1,7 @@
1
1
  import { Item } from "../items";
2
2
  import { PriKey } from "../keys";
3
3
  import { ItemQuery } from "../item/ItemQuery";
4
- import { AffectedKeys, AllOperationResult, AllOptions, OperationParams, Operations } from "./Operations";
4
+ import { AffectedKeys, AllOperationResult, AllOptions, FindOperationResult, FindOptions, OperationParams, Operations } from "./Operations";
5
5
  /**
6
6
  * Primary Operations interface - specialized for primary (top-level) items only.
7
7
  *
@@ -41,7 +41,7 @@ import { AffectedKeys, AllOperationResult, AllOptions, OperationParams, Operatio
41
41
  export interface PrimaryOperations<V extends Item<S>, S extends string> extends Omit<Operations<V, S>, 'all' | 'one' | 'create' | 'get' | 'update' | 'upsert' | 'remove' | 'find' | 'findOne' | 'action' | 'allAction' | 'facet' | 'allFacet'> {
42
42
  all(query?: ItemQuery, locations?: [], allOptions?: AllOptions): Promise<AllOperationResult<V>>;
43
43
  one(query?: ItemQuery): Promise<V | null>;
44
- find(finder: string, params?: OperationParams): Promise<V[]>;
44
+ find(finder: string, params?: OperationParams, locations?: [], options?: FindOptions): Promise<FindOperationResult<V>>;
45
45
  findOne(finder: string, params?: OperationParams): Promise<V | null>;
46
46
  create(item: Partial<Item<S>>, options?: {
47
47
  key?: PriKey<S>;
@@ -1,26 +1,32 @@
1
1
  /**
2
2
  * Wrapper for find() operation
3
3
  *
4
- * Provides automatic validation for find() operation parameters.
4
+ * Provides automatic validation for find() operation parameters and supports
5
+ * hybrid pagination approach:
6
+ * - If finder returns FindOperationResult<V>, uses it directly (opt-in)
7
+ * - If finder returns V[], applies post-processing pagination
5
8
  */
6
9
  import type { Item } from "../../items";
7
10
  import type { Coordinate } from "../../Coordinate";
8
11
  import type { FindMethod } from "../methods";
9
12
  import type { WrapperOptions } from "./types";
10
13
  /**
11
- * Creates a wrapped find() method with automatic parameter validation.
14
+ * Creates a wrapped find() method with automatic parameter validation and hybrid pagination support.
12
15
  *
13
16
  * @param coordinate - The coordinate defining the item hierarchy
14
17
  * @param implementation - The core logic for the operation
15
18
  * @param options - Optional configuration
16
- * @returns A fully validated find() method
19
+ * @returns A fully validated find() method that returns FindOperationResult<V>
17
20
  *
18
21
  * @example
19
22
  * ```typescript
20
23
  * const find = createFindWrapper(
21
24
  * coordinate,
22
- * async (finder, params, locations) => {
23
- * return await database.executeFinder(finder, params, locations);
25
+ * async (finder, params, locations, findOptions) => {
26
+ * const finderMethod = finders[finder];
27
+ * const result = await finderMethod(params, locations, findOptions);
28
+ * // Framework handles hybrid detection and post-processing if needed
29
+ * return result;
24
30
  * }
25
31
  * );
26
32
  * ```
@@ -7,9 +7,11 @@
7
7
  * - Items (Item key type validation)
8
8
  * - Queries (ItemQuery validation)
9
9
  * - Operation parameters (OperationParams validation)
10
+ * - Schema validation (Zod, Yup, etc.)
10
11
  */
11
- export type { ValidationOptions, ValidationResult } from './types';
12
+ export type { ValidationOptions, ValidationResult, SchemaValidator } from './types';
12
13
  export { validateLocations, isValidLocations } from './LocationValidator';
13
14
  export { validateKey, validatePriKey, validateComKey } from './KeyValidator';
14
15
  export { validatePK, validateKeys } from './ItemValidator';
15
16
  export { validateQuery, validateOperationParams, validateFinderName, validateActionName, validateFacetName } from './QueryValidator';
17
+ export { validateSchema } from './schema';
@@ -485,6 +485,96 @@ Received: "${facet}"`
485
485
  }
486
486
  logger5.debug(`Facet name validation passed for ${operation}`, { facet });
487
487
  };
488
+
489
+ // src/errors/ActionError.ts
490
+ var ActionError = class extends Error {
491
+ constructor(errorInfo, cause) {
492
+ super(errorInfo.message);
493
+ this.errorInfo = errorInfo;
494
+ this.name = "ActionError";
495
+ this.cause = cause;
496
+ if (!this.errorInfo.technical) {
497
+ this.errorInfo.technical = { timestamp: (/* @__PURE__ */ new Date()).toISOString() };
498
+ }
499
+ if (!this.errorInfo.technical.timestamp) {
500
+ this.errorInfo.technical.timestamp = (/* @__PURE__ */ new Date()).toISOString();
501
+ }
502
+ }
503
+ toJSON() {
504
+ return this.errorInfo;
505
+ }
506
+ };
507
+
508
+ // src/errors/ValidationError.ts
509
+ var ValidationError = class extends ActionError {
510
+ fieldErrors;
511
+ constructor(message, validOptions, suggestedAction, conflictingValue, fieldErrors) {
512
+ super({
513
+ code: "VALIDATION_ERROR",
514
+ message,
515
+ operation: { type: "create", name: "", params: {} },
516
+ // Will be filled by wrapper
517
+ context: { itemType: "" },
518
+ // Will be filled by wrapper
519
+ details: {
520
+ validOptions,
521
+ suggestedAction,
522
+ retryable: true,
523
+ conflictingValue,
524
+ fieldErrors
525
+ },
526
+ technical: {
527
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
528
+ }
529
+ });
530
+ this.fieldErrors = fieldErrors;
531
+ if (fieldErrors) {
532
+ if (!this.errorInfo.details) this.errorInfo.details = {};
533
+ this.errorInfo.details.fieldErrors = fieldErrors;
534
+ }
535
+ }
536
+ };
537
+
538
+ // src/validation/schema.ts
539
+ async function validateSchema(data, schema) {
540
+ if (!schema) {
541
+ return data;
542
+ }
543
+ try {
544
+ if (schema.parseAsync) {
545
+ return await schema.parseAsync(data);
546
+ }
547
+ const result = schema.safeParse(data);
548
+ if (result.success) {
549
+ return result.data;
550
+ } else {
551
+ throw result.error;
552
+ }
553
+ } catch (error) {
554
+ if (error && Array.isArray(error.issues)) {
555
+ const fieldErrors = error.issues.map((issue) => ({
556
+ path: issue.path,
557
+ message: issue.message,
558
+ code: issue.code
559
+ }));
560
+ const validationError = new ValidationError(
561
+ "Schema validation failed",
562
+ // eslint-disable-next-line no-undefined
563
+ void 0,
564
+ // eslint-disable-next-line no-undefined
565
+ void 0,
566
+ // eslint-disable-next-line no-undefined
567
+ void 0,
568
+ fieldErrors
569
+ );
570
+ throw validationError;
571
+ }
572
+ if (error instanceof ValidationError) {
573
+ throw error;
574
+ }
575
+ throw new ValidationError(`Validation failed: ${error.message || "Unknown error"}`);
576
+ }
577
+ }
488
578
  export {
489
579
  isValidLocations,
490
580
  validateActionName,
@@ -497,5 +587,6 @@ export {
497
587
  validateOperationParams,
498
588
  validatePK,
499
589
  validatePriKey,
500
- validateQuery
590
+ validateQuery,
591
+ validateSchema
501
592
  };
@@ -0,0 +1,23 @@
1
+ import { SchemaValidator } from "./types";
2
+ /**
3
+ * Validates data against a schema validator (Zod, Yup, etc.)
4
+ *
5
+ * Supports both synchronous and asynchronous validation.
6
+ * Transforms validator errors into Fjell ValidationError with FieldError[].
7
+ *
8
+ * @template T - The validated/parsed type
9
+ * @param data - The data to validate
10
+ * @param schema - Optional schema validator
11
+ * @returns Promise resolving to validated/parsed data
12
+ * @throws ValidationError if validation fails
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { validateSchema } from '@fjell/core/validation';
17
+ * import { z } from 'zod';
18
+ *
19
+ * const schema = z.object({ name: z.string() });
20
+ * const validated = await validateSchema({ name: 'Alice' }, schema);
21
+ * ```
22
+ */
23
+ export declare function validateSchema<T>(data: unknown, schema?: SchemaValidator<T>): Promise<T>;
@@ -36,3 +36,34 @@ export interface ValidationResult {
36
36
  */
37
37
  context?: Record<string, unknown>;
38
38
  }
39
+ /**
40
+ * Generic interface for schema validators (Zod, Yup, Joi, etc.)
41
+ *
42
+ * This interface enables duck typing - any validator that implements
43
+ * these methods will work with validateSchema.
44
+ *
45
+ * @template T - The validated/parsed type
46
+ */
47
+ export interface SchemaValidator<T> {
48
+ /**
49
+ * Synchronous parse method.
50
+ * Should throw an error if invalid, or return the parsed data.
51
+ */
52
+ parse: (data: unknown) => T;
53
+ /**
54
+ * Safe parse method that returns a result object instead of throwing.
55
+ * Used as fallback if parseAsync is not available.
56
+ */
57
+ safeParse: (data: unknown) => {
58
+ success: true;
59
+ data: T;
60
+ } | {
61
+ success: false;
62
+ error: any;
63
+ };
64
+ /**
65
+ * Optional asynchronous parse method.
66
+ * Preferred over parse/safeParse if available.
67
+ */
68
+ parseAsync?: (data: unknown) => Promise<T>;
69
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fjell/core",
3
3
  "description": "Core Item and Key Framework for Fjell",
4
- "version": "4.4.64",
4
+ "version": "4.4.65",
5
5
  "keywords": [
6
6
  "core",
7
7
  "fjell"