@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 +42 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +97 -11
- package/dist/operations/Operations.d.ts +100 -30
- package/dist/operations/contained.d.ts +2 -2
- package/dist/operations/methods.d.ts +52 -6
- package/dist/operations/primary.d.ts +2 -2
- package/dist/operations/wrappers/createFindWrapper.d.ts +11 -5
- package/dist/validation/index.d.ts +3 -1
- package/dist/validation/index.js +92 -1
- package/dist/validation/schema.d.ts +23 -0
- package/dist/validation/types.d.ts +31 -0
- package/package.json +1 -1
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 (
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
-
*
|
|
23
|
+
* Base options for pagination operations (all() and find()).
|
|
24
24
|
*
|
|
25
|
-
*
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
356
|
-
* const
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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';
|
package/dist/validation/index.js
CHANGED
|
@@ -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
|
+
}
|