@kalutskii/foundation 0.6.23 → 0.6.24

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
@@ -199,7 +199,34 @@ zodPaginationSchema.parse({ offset: '0', limit: '20' });
199
199
  // { offset: 0, limit: 20 }
200
200
  ```
201
201
 
202
- ### 9. Work with JWT using `ZodJWTService`
202
+ ### 9. Share bulk selection between frontend and backend
203
+
204
+ ```typescript
205
+ import { zodBulkSelectionSchema, zodSearchSchema } from '@kalutskii/foundation';
206
+ import { z } from 'zod';
207
+
208
+ const assetBulkSelectionSchema = zodBulkSelectionSchema({
209
+ identifierSchema: z.string().min(1),
210
+ });
211
+
212
+ const assetBulkFilterSchema = zodSearchSchema({
213
+ filters: assetSchema.pick({ status: true, categoryId: true }),
214
+ paginationEnabled: false,
215
+ });
216
+
217
+ const assetBulkRequestSchema = z
218
+ .object({
219
+ selection: assetBulkSelectionSchema,
220
+ filter: assetBulkFilterSchema,
221
+ })
222
+ .strict();
223
+
224
+ type AssetBulkRequest = z.infer<typeof assetBulkRequestSchema>;
225
+ ```
226
+
227
+ In `include` mode, the backend targets only `identifiers`. In `exclude` mode, it resolves all entities matching the same filter snapshot and removes `excludedIdentifiers` from that operation.
228
+
229
+ ### 10. Work with JWT using `ZodJWTService`
203
230
 
204
231
  ```typescript
205
232
  import { ZodJWTService } from '@kalutskii/foundation';
@@ -216,7 +243,7 @@ const payload = await jwt.verifyOrThrow(token, 'secret');
216
243
  const decoded = await jwt.decode(token);
217
244
  ```
218
245
 
219
- ### 10. Datetime helpers
246
+ ### 11. Datetime helpers
220
247
 
221
248
  ```typescript
222
249
  import { formatTime, getFormattedDate, getFormattedTime, getZonedTime } from '@kalutskii/foundation';
@@ -227,7 +254,7 @@ getFormattedDate({ tz: 'Europe/Moscow', withTime: false }); // '22.06.2026'
227
254
  formatTime(new Date(), { tz: 'Europe/Moscow' }); // '15:30:00, 22 июня 2026 (+3 UTC)'
228
255
  ```
229
256
 
230
- ### 11. Logging
257
+ ### 12. Logging
231
258
 
232
259
  ```typescript
233
260
  import { log } from '@kalutskii/foundation';
@@ -245,6 +272,7 @@ The package exports all public APIs from a single entrypoint:
245
272
  - **HTTP**: `success`, `failure`, `fetchSafely`, `fetchAndThrow`, constants, schemas.
246
273
  - **Hono**: `respond`, `onHandlerError`, `honoLoggingHandler`.
247
274
  - **Utilities**: `safeExecute`, `measureExecutionTime`, `generateRandomString`, `log`, `getColoredHTTPStatus`, datetime helpers.
275
+ - **Zod Bulk**: `zodBulkSelectionSchema`, `ZodBulkSelection`, `ZodBulkIncludeSelection`, `ZodBulkExcludeSelection`.
248
276
  - **Zod JWT**: `ZodJWTService`.
249
277
  - **Zod Search**: `zodSearchSchema`, `ZodSearchSchemaOptions`.
250
278
  - **Zod Pagination**: `zodPaginationSchema`, `zodPaginationShape`, `ZodPaginationOptions`.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SQL } from 'drizzle-orm';
2
2
  import { ErrorHandler, MiddlewareHandler, Context, TypedResponse } from 'hono';
3
3
  import { Locale } from 'date-fns';
4
- import { SymmetricAlgorithm } from 'hono/utils/jwt/jwa';
5
4
  import z, { ZodObject, ZodRawShape, z as z$1 } from 'zod';
5
+ import { SymmetricAlgorithm } from 'hono/utils/jwt/jwa';
6
6
 
7
7
  /**
8
8
  * Builds a Drizzle SQL `where` condition from a plain object.
@@ -222,6 +222,52 @@ type Simplify<T> = {
222
222
  [K in keyof T]: T[K];
223
223
  } & {};
224
224
 
225
+ /**
226
+ * Builds a strict selection contract for bulk operations across paginated data.
227
+ * The identifier schema is shared by explicit and all-matching selection modes.
228
+ *
229
+ * `include` targets only identifiers listed by the client. `exclude` targets every
230
+ * item matching the accompanying search snapshot except excluded identifiers.
231
+ *
232
+ * @example
233
+ * const assetBulkSelectionSchema = zodBulkSelectionSchema({
234
+ * identifierSchema: z.string().min(1),
235
+ * });
236
+ */
237
+ declare const zodBulkSelectionSchema: <const TIdentifierSchema extends z.ZodType<string | number>>({ identifierSchema, }: {
238
+ /**
239
+ * Schema used to validate every included or excluded entity identifier.
240
+ * Its transforms and exact inferred output are preserved in both branches.
241
+ */
242
+ identifierSchema: TIdentifierSchema;
243
+ }) => z.ZodDiscriminatedUnion<[z.ZodObject<{
244
+ mode: z.ZodLiteral<"include">;
245
+ identifiers: z.ZodArray<TIdentifierSchema>;
246
+ }, z.core.$strict>, z.ZodObject<{
247
+ mode: z.ZodLiteral<"exclude">;
248
+ excludedIdentifiers: z.ZodArray<TIdentifierSchema>;
249
+ }, z.core.$strict>], "mode">;
250
+
251
+ /**
252
+ * Shared bulk-selection payload produced by `zodBulkSelectionSchema`.
253
+ * String identifiers are used by default for frontend table integrations.
254
+ */
255
+ type ZodBulkSelection<TIdentifier extends string | number = string> = z.infer<ReturnType<typeof zodBulkSelectionSchema<z.ZodType<TIdentifier>>>>;
256
+ /**
257
+ * Explicit bulk selection containing only identifiers chosen by the client.
258
+ * This branch does not require resolving an all-matching search snapshot.
259
+ */
260
+ type ZodBulkIncludeSelection<TIdentifier extends string | number = string> = Extract<ZodBulkSelection<TIdentifier>, {
261
+ mode: 'include';
262
+ }>;
263
+ /**
264
+ * All-matching bulk selection containing identifiers excluded by the client.
265
+ * Backend handlers must resolve targets from the same search snapshot.
266
+ */
267
+ type ZodBulkExcludeSelection<TIdentifier extends string | number = string> = Extract<ZodBulkSelection<TIdentifier>, {
268
+ mode: 'exclude';
269
+ }>;
270
+
225
271
  /** Options for configuring the JWT service. */
226
272
  type JWTServiceOptions = {
227
273
  /** The algorithm to use for signing and verifying JWTs. Defaults to 'HS256'. */
@@ -297,15 +343,10 @@ declare const zodPaginationShape: {
297
343
  */
298
344
  type ZodPaginationOptions = z.infer<typeof zodPaginationSchema>;
299
345
  /**
300
- * Configuration used to compose a reusable search request schema.
301
- * Literal feature flags are preserved in the resulting static shape.
346
+ * Feature flags shared by both supported search schema composition modes.
347
+ * Literal values are preserved in the resulting runtime and static shapes.
302
348
  */
303
- type ZodSearchSchemaOptions<TShape extends z.ZodRawShape, TQueryEnabled extends boolean = true, TPaginationEnabled extends boolean = true> = {
304
- /**
305
- * Object schema whose fields become available inside the `where` object.
306
- * Every resulting field is optional, but one value must be defined.
307
- */
308
- filters: z.ZodObject<TShape>;
349
+ type ZodSearchFeatureOptions<TQueryEnabled extends boolean, TPaginationEnabled extends boolean> = {
309
350
  /**
310
351
  * Controls whether an optional non-empty `query` field is generated.
311
352
  * The field is enabled when this option is omitted from the call.
@@ -317,6 +358,30 @@ type ZodSearchSchemaOptions<TShape extends z.ZodRawShape, TQueryEnabled extends
317
358
  */
318
359
  paginationEnabled?: TPaginationEnabled;
319
360
  };
361
+ /**
362
+ * Configuration used to compose a reusable search request schema.
363
+ * Exactly one source must be provided for the resulting `where` field.
364
+ *
365
+ * `filters` is the concise mode that applies partial and non-empty rules.
366
+ * `whereSchema` accepts a fully prepared schema with custom Zod effects.
367
+ */
368
+ type ZodSearchSchemaOptions<TShape extends z.ZodRawShape = never, TWhereSchema extends z.ZodType<Record<string, unknown>> = never, TQueryEnabled extends boolean = true, TPaginationEnabled extends boolean = true> = ZodSearchFeatureOptions<TQueryEnabled, TPaginationEnabled> & ({
369
+ /**
370
+ * Object schema whose fields become optional filters inside `where`.
371
+ * The factory requires one defined value whenever `where` is present.
372
+ */
373
+ filters: z.ZodObject<TShape>;
374
+ /** Prevents combining automatic filters with a prepared schema. */
375
+ whereSchema?: never;
376
+ } | {
377
+ /** Prevents combining a prepared schema with automatic filters. */
378
+ filters?: never;
379
+ /**
380
+ * Prepared schema used directly to validate the `where` object.
381
+ * Its refinements, transforms, and inferred output are preserved.
382
+ */
383
+ whereSchema: TWhereSchema;
384
+ });
320
385
 
321
386
  /**
322
387
  * Optional text query shared by search request contracts.
@@ -325,20 +390,27 @@ type ZodSearchSchemaOptions<TShape extends z.ZodRawShape, TQueryEnabled extends
325
390
  declare const zodSearchQuerySchema: z.ZodOptional<z.ZodString>;
326
391
  /**
327
392
  * Makes every supplied filter optional while requiring one defined value.
328
- * The wrapper itself stays optional because searches may omit filtering.
393
+ * Top-level optionality is added later for both composition modes equally.
329
394
  */
330
- declare const createZodSearchWhereSchema: <TShape extends z.ZodRawShape>(filters: z.ZodObject<TShape>) => z.ZodOptional<z.ZodPipe<z.ZodObject<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, z.core.$strip>, z.ZodTransform<Awaited<AtLeastOne<z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>, keyof z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>, z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>>;
395
+ declare const createZodSearchWhereSchema: <TShape extends z.ZodRawShape>(filters: z.ZodObject<TShape>) => z.ZodPipe<z.ZodObject<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, z.core.$strip>, z.ZodTransform<Awaited<AtLeastOne<z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>, keyof z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>, z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>;
331
396
  /**
332
397
  * Empty schema shape used when a search feature is explicitly disabled.
333
398
  * Intersections remove disabled fields without widening enabled branches.
334
399
  */
335
400
  type ZodDisabledSearchShape = Record<never, never>;
336
401
  /**
337
- * Schema-level shape assembled from filter fields and literal feature flags.
402
+ * Resolves the schema used by `where` from the selected composition mode.
403
+ * Prepared schemas remain untouched, including their effects and output.
404
+ */
405
+ type ZodSearchWhereSchema<TShape extends z.ZodRawShape, TWhereSchema extends z.ZodType<Record<string, unknown>>> = [
406
+ TWhereSchema
407
+ ] extends [never] ? ReturnType<typeof createZodSearchWhereSchema<TShape>> : TWhereSchema;
408
+ /**
409
+ * Schema-level shape assembled from the selected source and feature flags.
338
410
  * Keeping Zod schemas here makes runtime composition the contract source.
339
411
  */
340
- type ZodSearchShape<TShape extends z.ZodRawShape, TQueryEnabled extends boolean, TPaginationEnabled extends boolean> = {
341
- where: ReturnType<typeof createZodSearchWhereSchema<TShape>>;
412
+ type ZodSearchShape<TShape extends z.ZodRawShape, TWhereSchema extends z.ZodType<Record<string, unknown>>, TQueryEnabled extends boolean, TPaginationEnabled extends boolean> = {
413
+ where: z.ZodOptional<ZodSearchWhereSchema<TShape, TWhereSchema>>;
342
414
  } & (TQueryEnabled extends false ? ZodDisabledSearchShape : {
343
415
  query: typeof zodSearchQuerySchema;
344
416
  }) & (TPaginationEnabled extends false ? ZodDisabledSearchShape : {
@@ -351,14 +423,19 @@ type ZodSearchShape<TShape extends z.ZodRawShape, TQueryEnabled extends boolean,
351
423
  * Query is optional and non-empty; pagination is required by default.
352
424
  * Literal feature flags update both runtime and inferred static shapes.
353
425
  *
426
+ * Pass `filters` for automatic partial and non-empty validation, or use
427
+ * `whereSchema` to preserve a prepared schema with custom Zod effects.
428
+ *
354
429
  * @example
355
430
  * const assetSearchSchema = zodSearchSchema({
356
431
  * filters: assetSchema.pick({ status: true }),
357
432
  * });
358
433
  *
359
- * type AssetSearch = z.infer<typeof assetSearchSchema>;
434
+ * const refinedAssetSearchSchema = zodSearchSchema({
435
+ * whereSchema: zodAtLeastOne(assetSchema.pick({ status: true }).partial()),
436
+ * });
360
437
  */
361
- declare const zodSearchSchema: <const TShape extends z.ZodRawShape, const TQueryEnabled extends boolean = true, const TPaginationEnabled extends boolean = true>({ filters, queryEnabled, paginationEnabled, }: ZodSearchSchemaOptions<TShape, TQueryEnabled, TPaginationEnabled>) => z.ZodObject<ZodSearchShape<TShape, TQueryEnabled, TPaginationEnabled> extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strict>;
438
+ declare const zodSearchSchema: <const TShape extends z.ZodRawShape = never, const TWhereSchema extends z.ZodType<Record<string, unknown>> = never, const TQueryEnabled extends boolean = true, const TPaginationEnabled extends boolean = true>(options: ZodSearchSchemaOptions<TShape, TWhereSchema, TQueryEnabled, TPaginationEnabled>) => z.ZodObject<ZodSearchShape<TShape, TWhereSchema, TQueryEnabled, TPaginationEnabled> extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strict>;
362
439
 
363
440
  type QueryPrimitive = string | number | boolean | bigint | Date;
364
441
  /**
@@ -421,4 +498,4 @@ declare const asQuery: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>
421
498
  */
422
499
  declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
423
500
 
424
- export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, asQuery, createStringEnumRecord, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, log, measureExecutionTime, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, zodAtLeastOne, zodPaginationSchema, zodPaginationShape, zodSearchSchema };
501
+ export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, type ZodBulkExcludeSelection, type ZodBulkIncludeSelection, type ZodBulkSelection, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, asQuery, createStringEnumRecord, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, log, measureExecutionTime, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, zodAtLeastOne, zodBulkSelectionSchema, zodPaginationSchema, zodPaginationShape, zodSearchSchema };
package/dist/index.js CHANGED
@@ -164,6 +164,19 @@ async function measureExecutionTime(execution) {
164
164
  return { result, executionTime: Math.round(executionTime) };
165
165
  }
166
166
 
167
+ // src/zod-bulk/zod-bulk.schemas.ts
168
+ import z from "zod";
169
+ var DEFAULT_ERROR_MESSAGE = "Selection identifiers must be provided.";
170
+ var zodBulkSelectionSchema = ({ identifierSchema }) => {
171
+ const identifiersSchema = z.array(identifierSchema).refine((identifiers) => new Set(identifiers).size === identifiers.length, {
172
+ message: DEFAULT_ERROR_MESSAGE
173
+ });
174
+ return z.discriminatedUnion("mode", [
175
+ z.object({ mode: z.literal("include"), identifiers: identifiersSchema }).strict(),
176
+ z.object({ mode: z.literal("exclude"), excludedIdentifiers: identifiersSchema }).strict()
177
+ ]);
178
+ };
179
+
167
180
  // src/zod-jwt/zod-jwt.schemas.ts
168
181
  import { decode as decodeJWT, sign as signJWT, verify as verifyJWT } from "hono/jwt";
169
182
  var ZodJWTService = class {
@@ -214,46 +227,47 @@ var ZodJWTService = class {
214
227
  };
215
228
 
216
229
  // src/zod-search/zod-search.pagination.schemas.ts
217
- import z from "zod";
218
- var zodPaginationSchema = z.object({
230
+ import z2 from "zod";
231
+ var zodPaginationSchema = z2.object({
219
232
  /**
220
233
  * Zero-based number of records skipped before collecting a result page.
221
234
  * String values are coerced to support validation of URL query input.
222
235
  */
223
- offset: z.coerce.number().int().nonnegative().default(0),
236
+ offset: z2.coerce.number().int().nonnegative().default(0),
224
237
  /**
225
238
  * Positive maximum number of records returned in a single result page.
226
239
  * String values are coerced to support validation of URL query input.
227
240
  */
228
- limit: z.coerce.number().int().positive().default(10)
241
+ limit: z2.coerce.number().int().positive().default(10)
229
242
  });
230
243
  var zodPaginationShape = zodPaginationSchema.shape;
231
244
 
232
245
  // src/zod-search/zod-search.schemas.ts
233
- import z2 from "zod";
246
+ import z3 from "zod";
234
247
 
235
248
  // src/zod-validation/zod-validation.refiners.ts
236
- var DEFAULT_ERROR_MESSAGE = "Invalid input. At least one field must be provided.";
249
+ var DEFAULT_ERROR_MESSAGE2 = "Invalid input. At least one field must be provided.";
237
250
  var zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
238
251
  const hasValue = Object.values(val).some((v) => v !== void 0);
239
252
  if (!hasValue)
240
- ctx.addIssue({ code: "custom", message: DEFAULT_ERROR_MESSAGE });
253
+ ctx.addIssue({ code: "custom", message: DEFAULT_ERROR_MESSAGE2 });
241
254
  }).transform((val) => val);
242
255
 
243
256
  // src/zod-search/zod-search.schemas.ts
244
- var zodSearchQuerySchema = z2.string().trim().min(1).optional();
245
- var createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial()).optional();
246
- var zodSearchSchema = ({ filters, queryEnabled, paginationEnabled }) => {
257
+ var zodSearchQuerySchema = z3.string().trim().min(1).optional();
258
+ var createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial());
259
+ var zodSearchSchema = (options) => {
260
+ const whereSchema = options.filters !== void 0 ? createZodSearchWhereSchema(options.filters) : options.whereSchema;
247
261
  const shape = {
248
- where: createZodSearchWhereSchema(filters),
249
- ...queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
250
- ...paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
262
+ where: whereSchema.optional(),
263
+ ...options.queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
264
+ ...options.paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
251
265
  };
252
- return z2.object(shape).strict();
266
+ return z3.object(shape).strict();
253
267
  };
254
268
 
255
269
  // src/zod-validation/zod-validation.parsing.ts
256
- import z3 from "zod";
270
+ import z4 from "zod";
257
271
 
258
272
  // src/zod-validation/zod-validation.utilities.ts
259
273
  var isPlainObject = (value) => {
@@ -280,7 +294,7 @@ var parseQueryValue = (value) => {
280
294
  return Number(normalized);
281
295
  return value;
282
296
  };
283
- var asQuery = (schema) => z3.preprocess(parseQueryValue, schema);
297
+ var asQuery = (schema) => z4.preprocess(parseQueryValue, schema);
284
298
  export {
285
299
  EXCEPTION_STATUS_CODES,
286
300
  SUCCESS_STATUS_CODES,
@@ -308,6 +322,7 @@ export {
308
322
  sqlWhere,
309
323
  success,
310
324
  zodAtLeastOne,
325
+ zodBulkSelectionSchema,
311
326
  zodPaginationSchema,
312
327
  zodPaginationShape,
313
328
  zodSearchSchema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kalutskii/foundation",
3
- "version": "0.6.23",
3
+ "version": "0.6.24",
4
4
  "description": "Typescript collection of most common utilities, schemas and functions among private projects.",
5
5
  "type": "module",
6
6
  "repository": {