@nanokajs/core 1.8.1 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.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, 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';
3
+ import { F as Field, I as InferFieldType, b as FieldPolicy, R as RelationDef } from './factories-D7estT7E.js';
4
+ export { a as FieldModifiers, c as IsRelationField, d as RelationFieldBuilder, e as RelationKind, f as RelationTargetLike, t } from './factories-D7estT7E.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';
@@ -20,6 +20,7 @@ import 'drizzle-orm/batch';
20
20
  declare function d1Adapter<TSchema extends Record<string, unknown> = Record<string, never>>(d1: D1Database): Adapter<TSchema>;
21
21
 
22
22
  type OpenAPISchemaObject = Record<string, unknown>;
23
+ type WithOpenAPIOption = Readonly<Record<string, true>>;
23
24
  interface OpenAPIModelComponent {
24
25
  create: OpenAPISchemaObject;
25
26
  update: OpenAPISchemaObject;
@@ -113,12 +114,13 @@ type WithResult<Fields extends Record<string, Field<any, any, any>>, With> = Row
113
114
  /**
114
115
  * Options for findMany query.
115
116
  * `limit` is required (no default).
116
- * `offset` defaults to 0 if omitted.
117
+ * `offset` defaults to 0 if omitted. Runtime-capped at 100_000 to prevent read amplification — use cursor pagination for deeper paging.
117
118
  * `orderBy` is optional for column ordering.
118
119
  * `where` accepts either an equality object or a Drizzle SQL expression.
119
120
  */
120
121
  interface FindManyOptions<Fields extends Record<string, Field<any, any, any>>, With extends WithOptions<Fields> | undefined = undefined> {
121
122
  readonly limit: number;
123
+ /** default: 0; runtime cap: 100000 */
122
124
  readonly offset?: number;
123
125
  readonly orderBy?: OrderBy<Fields>;
124
126
  readonly where?: Where<Fields> | SQL;
@@ -148,15 +150,26 @@ type OrderBy<Fields extends Record<string, Field<any, any, any>>> = (NonRelation
148
150
  readonly direction?: 'asc' | 'desc';
149
151
  }>;
150
152
  /**
151
- * Where clause object: each key is a field name, each value is the desired equality.
153
+ * A single where value: either an equality value or an `{ in: [...] }` operator for IN matching.
154
+ * Note: json fields whose stored value happens to be `{ in: [...] }` in shape are still treated
155
+ * as equality at runtime — the IN operator is applied to non-json fields only.
156
+ */
157
+ type WhereValue<T> = T | {
158
+ readonly in: readonly T[];
159
+ };
160
+ /**
161
+ * Where clause object: each key is a field name, each value is the desired equality, or an
162
+ * `{ in: [...] }` operator for IN matching.
163
+ * json フィールドの値が偶然 `{ in: [...] }` 形でも型上は演算子と区別できないが、ランタイムでは
164
+ * json フィールドは常に等値比較される(IN 演算子は非 json フィールドのみ)。
152
165
  */
153
166
  type Where<Fields extends Record<string, Field<any, any, any>>> = {
154
- readonly [K in NonRelationKeys<Fields>]?: InferFieldType<Fields[K]>;
167
+ readonly [K in NonRelationKeys<Fields>]?: WhereValue<InferFieldType<Fields[K]>>;
155
168
  };
156
169
  /**
157
- * Identifier or Where clause: can be a primary key value or a Where object.
170
+ * Identifier or Where clause: can be a primary key value, a Where object, or a Drizzle SQL expression.
158
171
  */
159
- type IdOrWhere<Fields extends Record<string, Field<any, any, any>>> = string | number | Where<Fields>;
172
+ type IdOrWhere<Fields extends Record<string, Field<any, any, any>>> = string | number | Where<Fields> | SQL;
160
173
  /**
161
174
  * Row type: full record type from fields, excluding relation fields.
162
175
  */
@@ -218,7 +231,7 @@ type ServerOnlyKeys<Fields extends Record<string, Field<any, any, any>>> = {
218
231
  /**
219
232
  * @internal
220
233
  * Determines if a field is required for create input.
221
- * A field is optional if it has a default, is optional, or is readOnly.
234
+ * A field is optional if it has a default, has defaultNow, is optional, or is readOnly.
222
235
  *
223
236
  * Uses direct shape matching (`Field<any, { hasDefault: true }, any>`) to avoid
224
237
  * the TypeScript infer-widening issue where `infer M` resolves to `FieldModifiers`
@@ -227,6 +240,8 @@ type ServerOnlyKeys<Fields extends Record<string, Field<any, any, any>>> = {
227
240
  */
228
241
  type IsRequired<F extends Field<any, any, any>> = F extends Field<any, {
229
242
  hasDefault: true;
243
+ }, any> ? false : F extends Field<any, {
244
+ defaultNow: true;
230
245
  }, any> ? false : F extends Field<any, {
231
246
  optional: true;
232
247
  }, any> ? false : F extends Field<any, {
@@ -237,7 +252,7 @@ type IsRequired<F extends Field<any, any, any>> = F extends Field<any, {
237
252
  * - `readOnly` fields are optional (library internals can pass them; API callers
238
253
  * are excluded via inputSchema — but CreateInput itself keeps them as optional
239
254
  * so handlers can supply generated values like `id` or `createdAt`)
240
- * - fields with `default` are optional
255
+ * - fields with `default` or `defaultNow` are optional (DB or runtime fills the value)
241
256
  * - fields marked `optional` are optional
242
257
  * - all other fields are required
243
258
  * - `serverOnly` fields are completely excluded (cannot be passed even internally)
@@ -467,11 +482,14 @@ interface Model<Fields extends Record<string, Field<any, any, any>>> {
467
482
  */
468
483
  toResponseMany(rows: readonly RowType<Fields>[]): unknown[];
469
484
  toOpenAPIComponent(): OpenAPIModelComponent;
485
+ toOpenAPISchema(usage: 'output', opts: {
486
+ readonly with: Partial<Record<RelationKeys<Fields> & string, true>>;
487
+ }): OpenAPISchemaObject;
470
488
  toOpenAPISchema(usage: OpenAPIUsage): OpenAPISchemaObject;
471
489
  /**
472
490
  * Fetches multiple rows with pagination and optional ordering.
473
491
  * `limit` is required (no default) to prevent accidental unbounded queries.
474
- * Offset defaults to 0 if omitted.
492
+ * Offset defaults to 0 if omitted. Runtime-capped at offset 100_000 — use cursor pagination for deeper paging.
475
493
  *
476
494
  * @example
477
495
  * const users = await User.findMany(adapter, { limit: 20 })
@@ -593,10 +611,14 @@ interface NanokaModel<Fields extends Record<string, Field<any, any, any>>> {
593
611
  toResponse(row: RowType<Fields>): unknown;
594
612
  toResponseMany(rows: readonly RowType<Fields>[]): unknown[];
595
613
  toOpenAPIComponent(): OpenAPIModelComponent;
614
+ toOpenAPISchema(usage: 'output', opts: {
615
+ readonly with: Partial<Record<RelationKeys<Fields> & string, true>>;
616
+ }): OpenAPISchemaObject;
596
617
  toOpenAPISchema(usage: OpenAPIUsage): OpenAPISchemaObject;
597
618
  /**
598
619
  * Fetches multiple rows with pagination and optional ordering.
599
620
  * `limit` is required (no default) to prevent accidental unbounded queries.
621
+ * Runtime-capped at offset 100_000 — use cursor pagination for deeper paging.
600
622
  */
601
623
  findMany<With extends WithOptions<Fields>>(options: FindManyOptions<Fields, With> & {
602
624
  readonly with: With;
@@ -702,4 +724,4 @@ interface Nanoka<E extends Env$1 = BlankEnv> extends Hono<E> {
702
724
  */
703
725
  declare function nanoka<E extends Env$1 = BlankEnv>(adapter: Adapter): Nanoka<E>;
704
726
 
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 };
727
+ 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 WithOpenAPIOption, type WithOptions, type WithResult, d1Adapter, defineModel, nanoka, swaggerUI };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- export { t } from './chunk-SHPR4NTZ.js';
1
+ export { t } from './chunk-UBDSGKTX.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, inArray } from 'drizzle-orm';
5
+ import { SQL, asc, desc, inArray, eq, and } 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';
@@ -154,6 +154,21 @@ function toOpenAPISchema(fields, usage, opts) {
154
154
  required.push(key);
155
155
  }
156
156
  }
157
+ if (usage === "output" && opts?.with !== void 0) {
158
+ for (const [key, field] of Object.entries(fields)) {
159
+ if (field.kind !== "relation") continue;
160
+ if (opts.with[key] !== true) continue;
161
+ const relationField = field;
162
+ const rawTarget = relationField.target;
163
+ const targetModel = typeof rawTarget === "function" ? rawTarget() : rawTarget;
164
+ const targetSchema = toOpenAPISchema(targetModel.fields, "output");
165
+ if (relationField.relationKind === "hasMany") {
166
+ properties[key] = { type: "array", items: targetSchema };
167
+ } else {
168
+ properties[key] = { ...targetSchema, nullable: true };
169
+ }
170
+ }
171
+ }
157
172
  return {
158
173
  type: "object",
159
174
  properties,
@@ -340,6 +355,7 @@ function buildTable(tableName, fields) {
340
355
  }
341
356
 
342
357
  // src/model/crud.ts
358
+ var MAX_OFFSET = 1e5;
343
359
  function guardLimit(limit) {
344
360
  if (!Number.isInteger(limit)) {
345
361
  throw new HTTPException(400, { message: "limit must be an integer" });
@@ -384,6 +400,9 @@ function findPrimaryKey(fields) {
384
400
  }
385
401
  return [pkKey, pkField];
386
402
  }
403
+ function isInOperator(value, field) {
404
+ return field?.kind !== "json" && typeof value === "object" && value !== null && !Array.isArray(value) && Object.hasOwn(value, "in") && Array.isArray(value.in);
405
+ }
387
406
  function buildWhereClause(table, fields, where) {
388
407
  const conditions = [];
389
408
  for (const [key, value] of Object.entries(where)) {
@@ -394,7 +413,12 @@ function buildWhereClause(table, fields, where) {
394
413
  throw new HTTPException(400, { message: "invalid column in table" });
395
414
  }
396
415
  const col = table[key];
397
- conditions.push(eq(col, value));
416
+ const field = fields[key];
417
+ if (isInOperator(value, field)) {
418
+ conditions.push(inArray(col, value.in));
419
+ } else {
420
+ conditions.push(eq(col, value));
421
+ }
398
422
  }
399
423
  if (conditions.length === 0) {
400
424
  return null;
@@ -508,6 +532,11 @@ async function loadRelations(adapter, _parentTable, parentFields, parentRows, wi
508
532
  async function findManyImpl(adapter, table, fields, options) {
509
533
  const limit = guardLimit(options.limit);
510
534
  const offset = options.offset !== void 0 ? guardOffset(options.offset) : 0;
535
+ if (offset > MAX_OFFSET) {
536
+ throw new HTTPException(400, {
537
+ message: `offset must be <= ${MAX_OFFSET}; use cursor pagination for deeper paging`
538
+ });
539
+ }
511
540
  let query = adapter.drizzle.select().from(table).limit(limit).offset(offset);
512
541
  if (options.where !== void 0) {
513
542
  if (options.where instanceof SQL) {
@@ -595,17 +624,22 @@ async function findAllImpl(adapter, table, fields, options) {
595
624
  return rows;
596
625
  }
597
626
  async function findOneImpl(adapter, table, fields, idOrWhere, options) {
598
- let where = null;
599
- if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
600
- const pk = findPrimaryKey(fields);
601
- if (!pk) {
602
- throw new HTTPException(500, { message: "model has no primary key" });
603
- }
604
- where = { [pk[0]]: idOrWhere };
627
+ let whereClause;
628
+ if (idOrWhere instanceof SQL) {
629
+ whereClause = idOrWhere;
605
630
  } else {
606
- where = idOrWhere;
631
+ let where;
632
+ if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
633
+ const pk = findPrimaryKey(fields);
634
+ if (!pk) {
635
+ throw new HTTPException(500, { message: "model has no primary key" });
636
+ }
637
+ where = { [pk[0]]: idOrWhere };
638
+ } else {
639
+ where = idOrWhere;
640
+ }
641
+ whereClause = buildWhereClause(table, fields, where);
607
642
  }
608
- const whereClause = buildWhereClause(table, fields, where);
609
643
  if (!whereClause) {
610
644
  throw new HTTPException(400, { message: "where clause must not be empty" });
611
645
  }
@@ -623,39 +657,51 @@ async function findOneImpl(adapter, table, fields, idOrWhere, options) {
623
657
  return rows[0];
624
658
  }
625
659
  async function createImpl(adapter, table, data) {
626
- const rows = await adapter.drizzle.insert(table).values(data).returning();
660
+ const builder = adapter.drizzle.insert(table).values(data).returning();
661
+ const rows = await builder;
627
662
  return rows[0];
628
663
  }
629
664
  async function updateImpl(adapter, table, fields, idOrWhere, data) {
630
- let where = null;
631
- if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
632
- const pk = findPrimaryKey(fields);
633
- if (!pk) {
634
- throw new HTTPException(500, { message: "model has no primary key" });
635
- }
636
- where = { [pk[0]]: idOrWhere };
665
+ let whereClause;
666
+ if (idOrWhere instanceof SQL) {
667
+ whereClause = idOrWhere;
637
668
  } else {
638
- where = idOrWhere;
669
+ let where;
670
+ if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
671
+ const pk = findPrimaryKey(fields);
672
+ if (!pk) {
673
+ throw new HTTPException(500, { message: "model has no primary key" });
674
+ }
675
+ where = { [pk[0]]: idOrWhere };
676
+ } else {
677
+ where = idOrWhere;
678
+ }
679
+ whereClause = buildWhereClause(table, fields, where);
639
680
  }
640
- const whereClause = buildWhereClause(table, fields, where);
641
681
  if (!whereClause) {
642
682
  throw new HTTPException(400, { message: "where clause must not be empty" });
643
683
  }
644
- const rows = await adapter.drizzle.update(table).set(data).where(whereClause).returning();
684
+ const builder = adapter.drizzle.update(table).set(data).where(whereClause).returning();
685
+ const rows = await builder;
645
686
  return rows.length > 0 ? rows[0] : null;
646
687
  }
647
688
  async function deleteImpl(adapter, table, fields, idOrWhere) {
648
- let where = null;
649
- if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
650
- const pk = findPrimaryKey(fields);
651
- if (!pk) {
652
- throw new HTTPException(500, { message: "model has no primary key" });
653
- }
654
- where = { [pk[0]]: idOrWhere };
689
+ let whereClause;
690
+ if (idOrWhere instanceof SQL) {
691
+ whereClause = idOrWhere;
655
692
  } else {
656
- where = idOrWhere;
693
+ let where;
694
+ if (typeof idOrWhere === "string" || typeof idOrWhere === "number") {
695
+ const pk = findPrimaryKey(fields);
696
+ if (!pk) {
697
+ throw new HTTPException(500, { message: "model has no primary key" });
698
+ }
699
+ where = { [pk[0]]: idOrWhere };
700
+ } else {
701
+ where = idOrWhere;
702
+ }
703
+ whereClause = buildWhereClause(table, fields, where);
657
704
  }
658
- const whereClause = buildWhereClause(table, fields, where);
659
705
  if (!whereClause) {
660
706
  throw new HTTPException(400, { message: "where clause must not be empty" });
661
707
  }
@@ -693,7 +739,20 @@ function defineModel(tableName, fields) {
693
739
  accessor
694
740
  );
695
741
  const baseSchema = buildBaseObject(fields);
696
- return applySchemaOptions(baseSchema, merged);
742
+ let schema = applySchemaOptions(baseSchema, merged);
743
+ if (usage === "create") {
744
+ const shape = schema.shape;
745
+ const patch = {};
746
+ for (const [key, field] of Object.entries(fields)) {
747
+ if (field.modifiers.defaultNow === true && field.modifiers.policy !== "readOnly" && key in shape) {
748
+ patch[key] = shape[key].optional();
749
+ }
750
+ }
751
+ if (Object.keys(patch).length > 0) {
752
+ schema = schema.extend(patch);
753
+ }
754
+ }
755
+ return schema;
697
756
  },
698
757
  outputSchema(opts) {
699
758
  const merged = derivePolicyOptions(fields, "output", opts, accessor);
@@ -710,8 +769,12 @@ function defineModel(tableName, fields) {
710
769
  toOpenAPIComponent() {
711
770
  return toOpenAPIComponent(fields);
712
771
  },
713
- toOpenAPISchema(usage) {
714
- return toOpenAPISchema(fields, usage);
772
+ toOpenAPISchema(usage, opts) {
773
+ return toOpenAPISchema(
774
+ fields,
775
+ usage,
776
+ opts ? { with: opts.with } : void 0
777
+ );
715
778
  },
716
779
  findMany: ((adapter, options) => findManyImpl(adapter, table, fields, options)),
717
780
  findAll(adapter, options) {
@@ -843,7 +906,10 @@ function nanoka(adapter) {
843
906
  toResponse: (row) => inner.toResponse(row),
844
907
  toResponseMany: (rows) => inner.toResponseMany(rows),
845
908
  toOpenAPIComponent: () => inner.toOpenAPIComponent(),
846
- toOpenAPISchema: (usage) => inner.toOpenAPISchema(usage),
909
+ toOpenAPISchema: (usage, opts) => (
910
+ // biome-ignore lint/suspicious/noExplicitAny: NanokaModel interface overloads provide type safety
911
+ inner.toOpenAPISchema(usage, opts)
912
+ ),
847
913
  findMany: ((options) => inner.findMany(adapter, options)),
848
914
  findAll: (options) => inner.findAll(adapter, options),
849
915
  findOne: ((idOrWhere, options) => inner.findOne(adapter, idOrWhere, options)),