@nanokajs/core 1.6.1 → 1.8.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/config.js CHANGED
@@ -1,4 +1,4 @@
1
- export { t } from './chunk-GZC7BVP2.js';
1
+ export { t } from './chunk-SHPR4NTZ.js';
2
2
 
3
3
  // src/config.ts
4
4
  function defineConfig(config) {
@@ -13,7 +13,29 @@ interface FieldModifiers {
13
13
  max?: number;
14
14
  policy?: FieldPolicy;
15
15
  }
16
- type FieldKind = 'string' | 'uuid' | 'number' | 'integer' | 'boolean' | 'timestamp' | 'json';
16
+ type FieldKind = 'string' | 'uuid' | 'number' | 'integer' | 'boolean' | 'timestamp' | 'json' | 'relation';
17
+ type RelationKind = 'hasMany' | 'belongsTo';
18
+ interface RelationTargetLike {
19
+ readonly fields: Record<string, {
20
+ kind: FieldKind;
21
+ zodBase: unknown;
22
+ drizzleColumn(name: string): unknown;
23
+ }>;
24
+ readonly tableName: string;
25
+ }
26
+ interface RelationDef<Target extends RelationTargetLike = RelationTargetLike, FK extends string = string> {
27
+ readonly kind: 'relation';
28
+ readonly relationKind: RelationKind;
29
+ readonly target: Target | (() => Target);
30
+ readonly foreignKey: FK;
31
+ readonly tsType: never;
32
+ readonly zodBase: z.ZodNever;
33
+ readonly modifiers: {};
34
+ drizzleColumn(name: string): never;
35
+ }
36
+ type IsRelationField<F> = F extends {
37
+ kind: 'relation';
38
+ } ? true : false;
17
39
  /**
18
40
  * Represents a typed database field with Zod validation and Drizzle column mapping.
19
41
  *
@@ -431,6 +453,17 @@ declare class JsonFieldBuilder<T = unknown, TS = T, Mods extends FieldModifiers
431
453
  policy: 'readOnly';
432
454
  }>;
433
455
  }
456
+ declare class RelationFieldBuilder<Target extends RelationTargetLike, FK extends string, Kind extends RelationKind> {
457
+ readonly kind: "relation";
458
+ readonly relationKind: Kind;
459
+ readonly target: Target | (() => Target);
460
+ readonly foreignKey: FK;
461
+ readonly tsType: never;
462
+ readonly modifiers: {};
463
+ constructor(relationKind: Kind, target: Target | (() => Target), foreignKey: FK);
464
+ get zodBase(): z.ZodNever;
465
+ drizzleColumn(name: string): never;
466
+ }
434
467
  declare function jsonField<S extends z.ZodTypeAny>(schema: S): JsonFieldBuilder<z.infer<S>, z.infer<S>, {}>;
435
468
  declare function jsonField<T = unknown>(): JsonFieldBuilder<T, T, {}>;
436
469
  /**
@@ -464,6 +497,12 @@ declare const t: {
464
497
  * Drizzle 型が欲しい場合は生成後に手動で書き換えるか `as` を使うこと。
465
498
  */
466
499
  json: typeof jsonField;
500
+ hasMany<Target extends RelationTargetLike, FK extends string>(target: Target | (() => Target), options: {
501
+ foreignKey: FK;
502
+ }): RelationFieldBuilder<Target, FK, "hasMany">;
503
+ belongsTo<Target extends RelationTargetLike, FK extends string>(target: Target | (() => Target), options: {
504
+ foreignKey: FK;
505
+ }): RelationFieldBuilder<Target, FK, "belongsTo">;
467
506
  };
468
507
 
469
- export { type Field as F, type InferFieldType as I, type FieldPolicy as a, type FieldModifiers as b, t };
508
+ export { type Field as F, type InferFieldType as I, type RelationDef as R, type FieldPolicy as a, type FieldModifiers as b, type IsRelationField as c, RelationFieldBuilder as d, type RelationKind as e, type RelationTargetLike as f, t };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { D1Database } from '@cloudflare/workers-types';
2
2
  import { A as Adapter } from './types-D2QDVyks.js';
3
- import { F as Field, I as InferFieldType, a as FieldPolicy } from './factories-Cmvx0495.js';
4
- export { b as FieldModifiers, t } from './factories-Cmvx0495.js';
3
+ import { F as Field, I as InferFieldType, a as FieldPolicy, R as RelationDef } from './factories-CiYoD7dl.js';
4
+ export { b as FieldModifiers, c as IsRelationField, d as RelationFieldBuilder, e as RelationKind, f as RelationTargetLike, t } from './factories-CiYoD7dl.js';
5
5
  import { Hook } from '@hono/zod-validator';
6
6
  import { SQL } from 'drizzle-orm';
7
7
  import { SQLiteTableWithColumns, AnySQLiteColumn } from 'drizzle-orm/sqlite-core';
@@ -81,6 +81,35 @@ interface OpenAPIDocument {
81
81
  readonly paths: Record<string, Record<string, unknown>>;
82
82
  }
83
83
 
84
+ /**
85
+ * @internal
86
+ * Extracts keys of non-relation fields from a Fields record.
87
+ * Relation fields (kind: 'relation') are excluded from DB column types, RowType, etc.
88
+ */
89
+ type NonRelationKeys<Fields extends Record<string, Field<any, any, any>>> = {
90
+ [K in keyof Fields]: Fields[K] extends {
91
+ kind: 'relation';
92
+ } ? never : K;
93
+ }[keyof Fields];
94
+ /**
95
+ * @internal
96
+ * Extracts keys of relation fields from a Fields record.
97
+ */
98
+ type RelationKeys<Fields extends Record<string, Field<any, any, any>>> = {
99
+ [K in keyof Fields]: Fields[K] extends {
100
+ kind: 'relation';
101
+ } ? K : never;
102
+ }[keyof Fields];
103
+ type RelationTargetFields<Target> = Target extends () => infer Resolved ? RelationTargetFields<Resolved> : Target extends {
104
+ fields: infer TargetFields;
105
+ } ? TargetFields : never;
106
+ type RelationResult<F> = F extends RelationDef<infer Target, string> ? RelationTargetFields<Target> extends Record<string, Field<any, any, any>> ? F['relationKind'] extends 'hasMany' ? RowType<RelationTargetFields<Target>>[] : RowType<RelationTargetFields<Target>> | null : never : never;
107
+ type WithOptions<Fields extends Record<string, Field<any, any, any>>> = {
108
+ readonly [K in RelationKeys<Fields>]?: true;
109
+ };
110
+ type WithResult<Fields extends Record<string, Field<any, any, any>>, With> = RowType<Fields> & {
111
+ readonly [K in keyof With]: RelationResult<Fields[K & keyof Fields]>;
112
+ };
84
113
  /**
85
114
  * Options for findMany query.
86
115
  * `limit` is required (no default).
@@ -88,11 +117,15 @@ interface OpenAPIDocument {
88
117
  * `orderBy` is optional for column ordering.
89
118
  * `where` accepts either an equality object or a Drizzle SQL expression.
90
119
  */
91
- interface FindManyOptions<Fields extends Record<string, Field<any, any, any>>> {
120
+ interface FindManyOptions<Fields extends Record<string, Field<any, any, any>>, With extends WithOptions<Fields> | undefined = undefined> {
92
121
  readonly limit: number;
93
122
  readonly offset?: number;
94
123
  readonly orderBy?: OrderBy<Fields>;
95
124
  readonly where?: Where<Fields> | SQL;
125
+ readonly with?: With;
126
+ }
127
+ interface FindOneOptions<Fields extends Record<string, Field<any, any, any>>, With extends WithOptions<Fields> | undefined = undefined> {
128
+ readonly with?: With;
96
129
  }
97
130
  /**
98
131
  * Options for findAll query.
@@ -107,38 +140,38 @@ interface FindAllOptions<Fields extends Record<string, Field<any, any, any>>> {
107
140
  /**
108
141
  * Order-by specification: single field, or field with direction, or array of multiple.
109
142
  */
110
- type OrderBy<Fields extends Record<string, Field<any, any, any>>> = (keyof Fields & string) | {
111
- readonly column: keyof Fields & string;
143
+ type OrderBy<Fields extends Record<string, Field<any, any, any>>> = (NonRelationKeys<Fields> & string) | {
144
+ readonly column: NonRelationKeys<Fields> & string;
112
145
  readonly direction?: 'asc' | 'desc';
113
- } | ReadonlyArray<(keyof Fields & string) | {
114
- readonly column: keyof Fields & string;
146
+ } | ReadonlyArray<(NonRelationKeys<Fields> & string) | {
147
+ readonly column: NonRelationKeys<Fields> & string;
115
148
  readonly direction?: 'asc' | 'desc';
116
149
  }>;
117
150
  /**
118
151
  * Where clause object: each key is a field name, each value is the desired equality.
119
152
  */
120
153
  type Where<Fields extends Record<string, Field<any, any, any>>> = {
121
- readonly [K in keyof Fields]?: InferFieldType<Fields[K]>;
154
+ readonly [K in NonRelationKeys<Fields>]?: InferFieldType<Fields[K]>;
122
155
  };
123
156
  /**
124
157
  * Identifier or Where clause: can be a primary key value or a Where object.
125
158
  */
126
159
  type IdOrWhere<Fields extends Record<string, Field<any, any, any>>> = string | number | Where<Fields>;
127
160
  /**
128
- * Row type: full record type from fields.
161
+ * Row type: full record type from fields, excluding relation fields.
129
162
  */
130
163
  type RowType<Fields extends Record<string, Field<any, any, any>>> = {
131
- [K in keyof Fields]: InferFieldType<Fields[K]>;
164
+ [K in NonRelationKeys<Fields>]: InferFieldType<Fields[K]>;
132
165
  };
133
166
  /**
134
- * Drizzle table type with columns typed to match the model's Fields.
167
+ * Drizzle table type with columns typed to match the model's Fields (excluding relation fields).
135
168
  * Allows type-safe column access via Model.table.fieldName.
136
169
  */
137
170
  type ModelTable<Fields extends Record<string, Field<any, any, any>>> = SQLiteTableWithColumns<{
138
171
  name: string;
139
172
  schema: undefined;
140
173
  columns: {
141
- [K in keyof Fields]: AnySQLiteColumn;
174
+ [K in NonRelationKeys<Fields>]: AnySQLiteColumn;
142
175
  };
143
176
  dialect: 'sqlite';
144
177
  }>;
@@ -209,11 +242,12 @@ type IsRequired<F extends Field<any, any, any>> = F extends Field<any, {
209
242
  * - all other fields are required
210
243
  * - `serverOnly` fields are completely excluded (cannot be passed even internally)
211
244
  * - `writeOnly` fields are included as required (they are input fields by design)
245
+ * - relation fields are completely excluded (no DB column)
212
246
  */
213
247
  type CreateInput<Fields extends Record<string, Field<any, any, any>>> = {
214
- [K in keyof Fields as IsRequired<Fields[K]> extends true ? K extends ServerOnlyKeys<Fields> ? never : K : never]: InferFieldType<Fields[K]>;
248
+ [K in NonRelationKeys<Fields> as IsRequired<Fields[K]> extends true ? K extends ServerOnlyKeys<Fields> ? never : K : never]: InferFieldType<Fields[K]>;
215
249
  } & {
216
- [K in keyof Fields as IsRequired<Fields[K]> extends true ? never : K extends ServerOnlyKeys<Fields> ? never : K]?: InferFieldType<Fields[K]>;
250
+ [K in NonRelationKeys<Fields> as IsRequired<Fields[K]> extends true ? never : K extends ServerOnlyKeys<Fields> ? never : K]?: InferFieldType<Fields[K]>;
217
251
  };
218
252
  type FieldAccessor<K extends string> = {
219
253
  readonly [Key in K]: Key;
@@ -226,11 +260,12 @@ interface SchemaOptions<K extends string = string> {
226
260
  /**
227
261
  * @internal
228
262
  * Extracts a Zod shape from fields for the Apply type.
229
- * Each field's zodBase becomes a property in the shape.
263
+ * Only non-relation fields are included (relation fields have zodBase: ZodNever
264
+ * and are skipped at runtime in buildBaseObject).
230
265
  * Indexed access ensures concrete Zod types are preserved (not `any`).
231
266
  */
232
267
  type FieldsToZodShape<Fields extends Record<string, Field<any, any, any>>> = {
233
- [K in keyof Fields]: Fields[K]['zodBase'];
268
+ [K in NonRelationKeys<Fields>]: Fields[K]['zodBase'];
234
269
  };
235
270
  /**
236
271
  * @internal
@@ -352,7 +387,7 @@ type ValidatorInput<Target extends keyof ValidationTargets, Schema extends z.Zod
352
387
  * Uses RowType<Fields> directly for input/output type computation
353
388
  * to ensure Zod v3/v4 compatibility (z.input<ZodTypeAny> returns unknown in Zod v4).
354
389
  */
355
- type ModelValidatorReturn<Fields extends Record<string, Field<any, any, any>>, Target extends keyof ValidationTargets, Opts extends SchemaOptions<keyof Fields & string> | undefined> = MiddlewareHandler<any, string, ValidatorInput<Target, Apply<FieldsToZodShape<Fields>, Opts>, ApplyToRecord<RowType<Fields>, Opts>, ApplyToRecord<RowType<Fields>, Opts>>>;
390
+ type ModelValidatorReturn<Fields extends Record<string, Field<any, any, any>>, Target extends keyof ValidationTargets, Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined> = MiddlewareHandler<any, string, ValidatorInput<Target, Apply<FieldsToZodShape<Fields>, Opts>, ApplyToRecord<RowType<Fields>, Opts>, ApplyToRecord<RowType<Fields>, Opts>>>;
356
391
 
357
392
  /**
358
393
  * Represents a database model with type-safe schema derivation and CRUD operations.
@@ -369,7 +404,7 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
369
404
  * const CreateSchema = User.schema({ omit: ['passwordHash'] })
370
405
  * const UpdateSchema = User.schema({ partial: true, pick: ['name', 'email'] })
371
406
  */
372
- schema<Opts extends SchemaOptions<keyof Fields & string> | undefined>(opts?: Opts): Apply<FieldsToZodShape<Fields>, Opts>;
407
+ schema<Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined>(opts?: Opts): Apply<FieldsToZodShape<Fields>, Opts>;
373
408
  /**
374
409
  * Returns a Hono middleware validator derived from this model's schema.
375
410
  * Integrates with @hono/zod-validator to validate request inputs.
@@ -393,7 +428,7 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
393
428
  */
394
429
  validator<Target extends keyof ValidationTargets, E extends Env = Env, P extends string = string>(target: Target, preset: 'create', hook?: Hook<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, E, P, Target>): MiddlewareHandler<E, P, ValidatorInput<Target, z.ZodObject<z.ZodRawShape>, Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>>>;
395
430
  validator<Target extends keyof ValidationTargets, E extends Env = Env, P extends string = string>(target: Target, preset: 'update', hook?: Hook<Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>, E, P, Target>): MiddlewareHandler<E, P, ValidatorInput<Target, z.ZodObject<z.ZodRawShape>, Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>, Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>>>;
396
- validator<Target extends keyof ValidationTargets, Opts extends SchemaOptions<keyof Fields & string> | undefined, E extends Env = Env, P extends string = string>(target: Target, opts?: Opts, hook?: Hook<z.output<Apply<FieldsToZodShape<Fields>, Opts>>, E, P, Target>): ModelValidatorReturn<Fields, Target, Opts>;
431
+ validator<Target extends keyof ValidationTargets, Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined, E extends Env = Env, P extends string = string>(target: Target, opts?: Opts, hook?: Hook<z.output<Apply<FieldsToZodShape<Fields>, Opts>>, E, P, Target>): ModelValidatorReturn<Fields, Target, Opts>;
397
432
  /**
398
433
  * Returns a Zod schema for API input, with policy-based field exclusions applied.
399
434
  * - 'create': serverOnly and readOnly fields are excluded
@@ -401,13 +436,13 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
401
436
  *
402
437
  * The return type precisely reflects policy-derived omissions using PolicyOmitKeys.
403
438
  */
404
- inputSchema<Opts extends SchemaOptions<keyof Fields & string> | undefined = undefined>(usage: 'create', opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, Opts>>;
405
- inputSchema<Opts extends SchemaOptions<keyof Fields & string> | undefined = undefined>(usage: 'update', opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>, Opts extends undefined ? {
439
+ inputSchema<Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined = undefined>(usage: 'create', opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, Opts>>;
440
+ inputSchema<Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined = undefined>(usage: 'update', opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>, Opts extends undefined ? {
406
441
  partial: true;
407
442
  } : Opts & {
408
443
  partial: true;
409
444
  }>>;
410
- inputSchema(usage: 'create' | 'update', opts?: SchemaOptions<keyof Fields & string>): z.ZodObject<z.ZodRawShape>;
445
+ inputSchema(usage: 'create' | 'update', opts?: SchemaOptions<NonRelationKeys<Fields> & string>): z.ZodObject<z.ZodRawShape>;
411
446
  /**
412
447
  * Returns a Zod schema for API output, with policy-based field exclusions applied.
413
448
  * - serverOnly and writeOnly fields are excluded
@@ -415,7 +450,7 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
415
450
  *
416
451
  * The return type precisely reflects policy-derived omissions using PolicyOmitKeys.
417
452
  */
418
- outputSchema<Opts extends SchemaOptions<keyof Fields & string> | undefined = undefined>(opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'output'> & string>, Opts>>;
453
+ outputSchema<Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined = undefined>(opts?: Opts): z.ZodObject<ApplyShape<Omit<FieldsToZodShape<Fields>, PolicyOmitKeys<Fields, 'output'> & string>, Opts>>;
419
454
  /**
420
455
  * Parses a DB row through outputSchema and returns the safe response object.
421
456
  * Equivalent to `User.outputSchema().parse(row)`.
@@ -443,6 +478,9 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
443
478
  * const page2 = await User.findMany(adapter, { limit: 20, offset: 20 })
444
479
  * const sorted = await User.findMany(adapter, { limit: 10, orderBy: 'name' })
445
480
  */
481
+ findMany<With extends WithOptions<Fields>>(adapter: Adapter, options: FindManyOptions<Fields, With> & {
482
+ readonly with: With;
483
+ }): Promise<WithResult<Fields, With>[]>;
446
484
  findMany(adapter: Adapter, options: FindManyOptions<Fields>): Promise<RowType<Fields>[]>;
447
485
  /**
448
486
  * Fetches all rows without a LIMIT clause.
@@ -461,6 +499,9 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
461
499
  * const user = await User.findOne(adapter, id)
462
500
  * const byEmail = await User.findOne(adapter, { email: 'john@example.com' })
463
501
  */
502
+ findOne<With extends WithOptions<Fields>>(adapter: Adapter, idOrWhere: IdOrWhere<Fields>, options: FindOneOptions<Fields, With> & {
503
+ readonly with: With;
504
+ }): Promise<WithResult<Fields, With> | null>;
464
505
  findOne(adapter: Adapter, idOrWhere: IdOrWhere<Fields>): Promise<RowType<Fields> | null>;
465
506
  /**
466
507
  * Creates a new row and returns the created record.
@@ -529,7 +570,7 @@ interface NanokaModel<Fields extends Record<string, Field<any, any, any>>> {
529
570
  * Returns a Zod schema derived from this model's fields.
530
571
  * Supports pick, omit, and partial transformations.
531
572
  */
532
- schema<Opts extends SchemaOptions<keyof Fields & string> | undefined = undefined>(opts?: Opts): Apply<FieldsToZodShape<Fields>, Opts>;
573
+ schema<Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined = undefined>(opts?: Opts): Apply<FieldsToZodShape<Fields>, Opts>;
533
574
  /**
534
575
  * Returns a Hono middleware validator for this model.
535
576
  * The optional `hook` argument is passed through to @hono/zod-validator.
@@ -537,15 +578,15 @@ interface NanokaModel<Fields extends Record<string, Field<any, any, any>>> {
537
578
  */
538
579
  validator<Target extends keyof hono.ValidationTargets, E extends Env$1 = Env$1, P extends string = string>(target: Target, preset: 'create', hook?: Hook<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, E, P, Target>): MiddlewareHandler<E, P, ValidatorInput<Target, z.ZodObject<z.ZodRawShape>, Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>, Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-create'> & string>>>;
539
580
  validator<Target extends keyof hono.ValidationTargets, E extends Env$1 = Env$1, P extends string = string>(target: Target, preset: 'update', hook?: Hook<Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>, E, P, Target>): MiddlewareHandler<E, P, ValidatorInput<Target, z.ZodObject<z.ZodRawShape>, Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>, Partial<Omit<RowType<Fields>, PolicyOmitKeys<Fields, 'input-update'> & string>>>>;
540
- validator<Target extends keyof hono.ValidationTargets, Opts extends SchemaOptions<keyof Fields & string> | undefined = undefined, E extends Env$1 = Env$1, P extends string = string>(target: Target, opts?: Opts, hook?: Hook<z.output<Apply<FieldsToZodShape<Fields>, Opts>>, E, P, Target>): ModelValidatorReturn<Fields, Target, Opts>;
581
+ validator<Target extends keyof hono.ValidationTargets, Opts extends SchemaOptions<NonRelationKeys<Fields> & string> | undefined = undefined, E extends Env$1 = Env$1, P extends string = string>(target: Target, opts?: Opts, hook?: Hook<z.output<Apply<FieldsToZodShape<Fields>, Opts>>, E, P, Target>): ModelValidatorReturn<Fields, Target, Opts>;
541
582
  /**
542
583
  * Returns a Zod schema for API input, with policy-based field exclusions applied.
543
584
  */
544
- inputSchema(usage: 'create' | 'update', opts?: SchemaOptions<keyof Fields & string>): z.ZodObject<z.ZodRawShape>;
585
+ inputSchema(usage: 'create' | 'update', opts?: SchemaOptions<NonRelationKeys<Fields> & string>): z.ZodObject<z.ZodRawShape>;
545
586
  /**
546
587
  * Returns a Zod schema for API output, with policy-based field exclusions applied.
547
588
  */
548
- outputSchema(opts?: SchemaOptions<keyof Fields & string>): z.ZodObject<z.ZodRawShape>;
589
+ outputSchema(opts?: SchemaOptions<NonRelationKeys<Fields> & string>): z.ZodObject<z.ZodRawShape>;
549
590
  /**
550
591
  * Parses a DB row through outputSchema and returns the safe response object.
551
592
  */
@@ -557,6 +598,9 @@ interface NanokaModel<Fields extends Record<string, Field<any, any, any>>> {
557
598
  * Fetches multiple rows with pagination and optional ordering.
558
599
  * `limit` is required (no default) to prevent accidental unbounded queries.
559
600
  */
601
+ findMany<With extends WithOptions<Fields>>(options: FindManyOptions<Fields, With> & {
602
+ readonly with: With;
603
+ }): Promise<WithResult<Fields, With>[]>;
560
604
  findMany(options: FindManyOptions<Fields>): Promise<RowType<Fields>[]>;
561
605
  /**
562
606
  * Fetches all rows without a LIMIT clause.
@@ -567,6 +611,9 @@ interface NanokaModel<Fields extends Record<string, Field<any, any, any>>> {
567
611
  * Fetches a single row by primary key or where clause.
568
612
  * Returns null if not found.
569
613
  */
614
+ findOne<With extends WithOptions<Fields>>(idOrWhere: IdOrWhere<Fields>, options: FindOneOptions<Fields, With> & {
615
+ readonly with: With;
616
+ }): Promise<WithResult<Fields, With> | null>;
570
617
  findOne(idOrWhere: IdOrWhere<Fields>): Promise<RowType<Fields> | null>;
571
618
  /**
572
619
  * Creates a new row and returns the created record.
@@ -655,4 +702,4 @@ interface Nanoka<E extends Env$1 = BlankEnv> extends Hono<E> {
655
702
  */
656
703
  declare function nanoka<E extends Env$1 = BlankEnv>(adapter: Adapter): Nanoka<E>;
657
704
 
658
- export { Adapter, type CreateInput, Field, FieldPolicy, type FindAllOptions, type FindManyOptions, type HttpMethod, type IdOrWhere, InferFieldType, type Model, type Nanoka, type NanokaModel, type OpenAPIDocument, type OpenAPIModelComponent, type OpenAPIParamSpec, type OpenAPIRequestBodySpec, type OpenAPIResponseSpec, type OpenAPIRouteMetadata, type OpenAPISchemaObject, type OpenAPISpecOptions, type OpenAPIUsage, type OrderBy, type RouteOpenAPIMetadata, type RouteOpenAPIOption, type RowType, type SchemaOptions, type Where, d1Adapter, defineModel, nanoka, swaggerUI };
705
+ export { Adapter, type CreateInput, Field, FieldPolicy, type FindAllOptions, type FindManyOptions, type FindOneOptions, type HttpMethod, type IdOrWhere, InferFieldType, type Model, type Nanoka, type NanokaModel, type OpenAPIDocument, type OpenAPIModelComponent, type OpenAPIParamSpec, type OpenAPIRequestBodySpec, type OpenAPIResponseSpec, type OpenAPIRouteMetadata, type OpenAPISchemaObject, type OpenAPISpecOptions, type OpenAPIUsage, type OrderBy, RelationDef, type RelationKeys, type RouteOpenAPIMetadata, type RouteOpenAPIOption, type RowType, type SchemaOptions, type Where, type WithOptions, type WithResult, d1Adapter, defineModel, nanoka, swaggerUI };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- export { t } from './chunk-GZC7BVP2.js';
1
+ export { t } from './chunk-SHPR4NTZ.js';
2
2
  import { drizzle } from 'drizzle-orm/d1';
3
3
  import { zValidator } from '@hono/zod-validator';
4
4
  import { z } from 'zod';
5
- import { SQL, asc, desc, eq, and } from 'drizzle-orm';
5
+ import { SQL, asc, desc, eq, and, inArray } from 'drizzle-orm';
6
6
  import { HTTPException } from 'hono/http-exception';
7
7
  import { sqliteTable } from 'drizzle-orm/sqlite-core';
8
8
  import { Hono } from 'hono';
@@ -23,7 +23,10 @@ function resolveSchemaOptionKeys(opt, accessor) {
23
23
  }
24
24
  function buildFieldAccessor(fields) {
25
25
  const acc = {};
26
- for (const k of Object.keys(fields)) acc[k] = k;
26
+ for (const [k, field] of Object.entries(fields)) {
27
+ if (field.kind === "relation") continue;
28
+ acc[k] = k;
29
+ }
27
30
  return Object.freeze(acc);
28
31
  }
29
32
 
@@ -37,6 +40,13 @@ function computeForcedOmit(fields) {
37
40
  }
38
41
  return omit;
39
42
  }
43
+ function computeRelationOmit(fields) {
44
+ const keys = /* @__PURE__ */ new Set();
45
+ for (const [key, field] of Object.entries(fields)) {
46
+ if (field.kind === "relation") keys.add(key);
47
+ }
48
+ return keys;
49
+ }
40
50
  function computeOptionalOmit(fields, target) {
41
51
  const omit = [];
42
52
  for (const [key, field] of Object.entries(fields)) {
@@ -51,7 +61,9 @@ function computeOptionalOmit(fields, target) {
51
61
  return omit;
52
62
  }
53
63
  function derivePolicyOptions(fields, usage, userOpts, accessor) {
54
- const forced = computeForcedOmit(fields);
64
+ const forcedBase = computeForcedOmit(fields);
65
+ const relationOmit = computeRelationOmit(fields);
66
+ const forced = [.../* @__PURE__ */ new Set([...forcedBase, ...relationOmit])];
55
67
  const optional = computeOptionalOmit(fields, usage);
56
68
  const acc = accessor ?? {};
57
69
  const resolvedPick = resolveSchemaOptionKeys(userOpts?.pick, acc);
@@ -79,6 +91,7 @@ function derivePolicyOptions(fields, usage, userOpts, accessor) {
79
91
  function buildBaseObject(fields) {
80
92
  const shape = {};
81
93
  for (const [key, field] of Object.entries(fields)) {
94
+ if (field.kind === "relation") continue;
82
95
  shape[key] = field.zodBase;
83
96
  }
84
97
  return z.object(shape);
@@ -129,6 +142,7 @@ function toOpenAPISchema(fields, usage, opts) {
129
142
  for (const [key, zodSchema] of Object.entries(shape)) {
130
143
  const field = fields[key];
131
144
  if (field === void 0) continue;
145
+ if (field.kind === "relation") continue;
132
146
  properties[key] = fieldToOpenAPISchema(field, opts?.strict);
133
147
  if (field.modifiers.policy === "writeOnly" && (usage === "create" || usage === "update")) {
134
148
  properties[key].writeOnly = true;
@@ -316,6 +330,16 @@ function getZodCheckDef(check) {
316
330
  }
317
331
  return check;
318
332
  }
333
+ function buildTable(tableName, fields) {
334
+ const columns = {};
335
+ for (const [key, field] of Object.entries(fields)) {
336
+ if (field.kind === "relation") continue;
337
+ columns[key] = field.drizzleColumn(key);
338
+ }
339
+ return sqliteTable(tableName, columns);
340
+ }
341
+
342
+ // src/model/crud.ts
319
343
  function guardLimit(limit) {
320
344
  if (!Number.isInteger(limit)) {
321
345
  throw new HTTPException(400, { message: "limit must be an integer" });
@@ -380,6 +404,143 @@ function buildWhereClause(table, fields, where) {
380
404
  }
381
405
  return and(...conditions);
382
406
  }
407
+ function resolveRelationTarget(field) {
408
+ return typeof field.target === "function" ? field.target() : field.target;
409
+ }
410
+ function getTargetTable(target) {
411
+ if (Object.hasOwn(target, "table")) {
412
+ return target.table;
413
+ }
414
+ return buildTable(target.tableName, target.fields);
415
+ }
416
+ function validateWithOptions(fields, withOptions) {
417
+ if (withOptions === null || typeof withOptions !== "object" || Array.isArray(withOptions)) {
418
+ throw new HTTPException(400, { message: "invalid with option" });
419
+ }
420
+ for (const [key, value] of Object.entries(withOptions)) {
421
+ if (value !== true) {
422
+ throw new HTTPException(400, { message: "nested with is not supported" });
423
+ }
424
+ if (!Object.hasOwn(fields, key) || fields[key]?.kind !== "relation") {
425
+ throw new HTTPException(400, { message: "invalid relation in with option" });
426
+ }
427
+ }
428
+ }
429
+ var relationCycleCache = /* @__PURE__ */ new WeakMap();
430
+ function assertNoRelationCycle(fields, rootName = "root") {
431
+ const cached = relationCycleCache.get(fields);
432
+ if (cached !== void 0) {
433
+ if (cached !== null) throw cached;
434
+ return;
435
+ }
436
+ const visiting = /* @__PURE__ */ new Set();
437
+ const visited = /* @__PURE__ */ new Set();
438
+ const visit = (currentFields, path) => {
439
+ if (visiting.has(currentFields)) {
440
+ return new HTTPException(500, { message: "relation cycle detected" });
441
+ }
442
+ if (visited.has(currentFields)) {
443
+ return null;
444
+ }
445
+ visiting.add(currentFields);
446
+ for (const field of Object.values(currentFields)) {
447
+ if (field.kind !== "relation") continue;
448
+ const target = resolveRelationTarget(field);
449
+ const targetFields = target.fields;
450
+ const error2 = visit(targetFields, [...path, target.tableName]);
451
+ if (error2 !== null) {
452
+ return error2;
453
+ }
454
+ }
455
+ visiting.delete(currentFields);
456
+ visited.add(currentFields);
457
+ return null;
458
+ };
459
+ const error = visit(fields, [rootName]);
460
+ relationCycleCache.set(fields, error);
461
+ if (error !== null) {
462
+ throw error;
463
+ }
464
+ }
465
+ async function loadRelations(adapter, _parentTable, parentFields, parentRows, withOptions) {
466
+ if (parentRows.length === 0) {
467
+ return [];
468
+ }
469
+ const rows = parentRows.map((row) => ({ ...row }));
470
+ for (const relationKey of Object.keys(withOptions)) {
471
+ const field = parentFields[relationKey];
472
+ if (field?.kind !== "relation") {
473
+ throw new HTTPException(400, { message: "invalid relation in with option" });
474
+ }
475
+ const relation = field;
476
+ const target = resolveRelationTarget(relation);
477
+ const targetFields = target.fields;
478
+ const targetTable = getTargetTable(target);
479
+ if (relation.relationKind === "hasMany") {
480
+ const parentPk = findPrimaryKey(parentFields);
481
+ if (!parentPk) {
482
+ throw new HTTPException(500, { message: "model has no primary key" });
483
+ }
484
+ const parentPkKey = parentPk[0];
485
+ const foreignKey = relation.foreignKey;
486
+ if (!Object.hasOwn(targetFields, foreignKey) || !Object.hasOwn(targetTable, foreignKey)) {
487
+ throw new HTTPException(500, { message: "invalid relation foreign key" });
488
+ }
489
+ const parentIds = Array.from(
490
+ new Set(parentRows.map((row) => row[parentPkKey]))
491
+ );
492
+ const targetRows = parentIds.length === 0 ? [] : await adapter.drizzle.select().from(targetTable).where(
493
+ inArray(
494
+ targetTable[foreignKey],
495
+ parentIds
496
+ )
497
+ );
498
+ const grouped = /* @__PURE__ */ new Map();
499
+ for (const targetRow of targetRows) {
500
+ const value = targetRow[foreignKey];
501
+ const group = grouped.get(value);
502
+ if (group === void 0) {
503
+ grouped.set(value, [targetRow]);
504
+ } else {
505
+ group.push(targetRow);
506
+ }
507
+ }
508
+ for (const row of rows) {
509
+ row[relationKey] = grouped.get(row[parentPkKey]) ?? [];
510
+ }
511
+ } else {
512
+ const foreignKey = relation.foreignKey;
513
+ if (!Object.hasOwn(parentFields, foreignKey) || !Object.hasOwn(_parentTable, foreignKey)) {
514
+ throw new HTTPException(500, { message: "invalid relation foreign key" });
515
+ }
516
+ const targetPk = findPrimaryKey(targetFields);
517
+ if (!targetPk) {
518
+ throw new HTTPException(500, { message: "model has no primary key" });
519
+ }
520
+ const targetPkKey = targetPk[0];
521
+ if (!Object.hasOwn(targetTable, targetPkKey)) {
522
+ throw new HTTPException(500, { message: "invalid relation primary key" });
523
+ }
524
+ const parentFkValues = Array.from(
525
+ new Set(parentRows.map((row) => row[foreignKey]))
526
+ );
527
+ const targetRows = parentFkValues.length === 0 ? [] : await adapter.drizzle.select().from(targetTable).where(
528
+ inArray(
529
+ targetTable[targetPkKey],
530
+ parentFkValues
531
+ )
532
+ );
533
+ const byPk = /* @__PURE__ */ new Map();
534
+ for (const targetRow of targetRows) {
535
+ byPk.set(targetRow[targetPkKey], targetRow);
536
+ }
537
+ for (const row of rows) {
538
+ row[relationKey] = byPk.get(row[foreignKey]) ?? null;
539
+ }
540
+ }
541
+ }
542
+ return rows;
543
+ }
383
544
  async function findManyImpl(adapter, table, fields, options) {
384
545
  const limit = guardLimit(options.limit);
385
546
  const offset = options.offset !== void 0 ? guardOffset(options.offset) : 0;
@@ -421,6 +582,11 @@ async function findManyImpl(adapter, table, fields, options) {
421
582
  }
422
583
  }
423
584
  const rows = await query;
585
+ if (options.with !== void 0) {
586
+ validateWithOptions(fields, options.with);
587
+ assertNoRelationCycle(fields);
588
+ return loadRelations(adapter, table, fields, rows, options.with);
589
+ }
424
590
  return rows;
425
591
  }
426
592
  async function findAllImpl(adapter, table, fields, options) {
@@ -465,7 +631,7 @@ async function findAllImpl(adapter, table, fields, options) {
465
631
  const rows = await query;
466
632
  return rows;
467
633
  }
468
- async function findOneImpl(adapter, table, fields, idOrWhere) {
634
+ async function findOneImpl(adapter, table, fields, idOrWhere, options) {
469
635
  let where = null;
470
636
  if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
471
637
  const pk = findPrimaryKey(fields);
@@ -480,8 +646,19 @@ async function findOneImpl(adapter, table, fields, idOrWhere) {
480
646
  if (!whereClause) {
481
647
  throw new HTTPException(400, { message: "where clause must not be empty" });
482
648
  }
649
+ if (options?.with !== void 0) {
650
+ validateWithOptions(fields, options.with);
651
+ assertNoRelationCycle(fields);
652
+ }
483
653
  const rows = await adapter.drizzle.select().from(table).where(whereClause).limit(1);
484
- return rows.length > 0 ? rows[0] : null;
654
+ if (rows.length === 0) {
655
+ return null;
656
+ }
657
+ if (options?.with !== void 0) {
658
+ const withRows = await loadRelations(adapter, table, fields, [rows[0]], options.with);
659
+ return withRows[0] ?? null;
660
+ }
661
+ return rows[0];
485
662
  }
486
663
  async function createImpl(adapter, table, data) {
487
664
  const rows = await adapter.drizzle.insert(table).values(data).returning();
@@ -523,13 +700,6 @@ async function deleteImpl(adapter, table, fields, idOrWhere) {
523
700
  const result = await adapter.drizzle.delete(table).where(whereClause).returning();
524
701
  return { deleted: result.length };
525
702
  }
526
- function buildTable(tableName, fields) {
527
- const columns = {};
528
- for (const [key, field] of Object.entries(fields)) {
529
- columns[key] = field.drizzleColumn(key);
530
- }
531
- return sqliteTable(tableName, columns);
532
- }
533
703
 
534
704
  // src/model/define.ts
535
705
  function defineModel(tableName, fields) {
@@ -581,15 +751,11 @@ function defineModel(tableName, fields) {
581
751
  toOpenAPISchema(usage) {
582
752
  return toOpenAPISchema(fields, usage);
583
753
  },
584
- findMany(adapter, options) {
585
- return findManyImpl(adapter, table, fields, options);
586
- },
754
+ findMany: ((adapter, options) => findManyImpl(adapter, table, fields, options)),
587
755
  findAll(adapter, options) {
588
756
  return findAllImpl(adapter, table, fields, options);
589
757
  },
590
- findOne(adapter, idOrWhere) {
591
- return findOneImpl(adapter, table, fields, idOrWhere);
592
- },
758
+ findOne: ((adapter, idOrWhere, options) => findOneImpl(adapter, table, fields, idOrWhere, options)),
593
759
  create(adapter, data) {
594
760
  return createImpl(adapter, table, data);
595
761
  },
@@ -716,9 +882,9 @@ function nanoka(adapter) {
716
882
  toResponseMany: (rows) => inner.toResponseMany(rows),
717
883
  toOpenAPIComponent: () => inner.toOpenAPIComponent(),
718
884
  toOpenAPISchema: (usage) => inner.toOpenAPISchema(usage),
719
- findMany: (options) => inner.findMany(adapter, options),
885
+ findMany: ((options) => inner.findMany(adapter, options)),
720
886
  findAll: (options) => inner.findAll(adapter, options),
721
- findOne: (idOrWhere) => inner.findOne(adapter, idOrWhere),
887
+ findOne: ((idOrWhere, options) => inner.findOne(adapter, idOrWhere, options)),
722
888
  create: (data) => inner.create(adapter, data),
723
889
  update: (idOrWhere, data) => inner.update(adapter, idOrWhere, data),
724
890
  delete: (idOrWhere) => inner.delete(adapter, idOrWhere)