@nanokajs/core 0.1.4 → 1.0.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.js CHANGED
@@ -1,9 +1,9 @@
1
- export { t } from './chunk-F5OBRXXY.js';
1
+ export { t } from './chunk-UYXRKFAR.js';
2
2
  import { drizzle } from 'drizzle-orm/d1';
3
3
  import { zValidator } from '@hono/zod-validator';
4
+ import { z } from 'zod';
4
5
  import { asc, desc, eq, and } from 'drizzle-orm';
5
6
  import { HTTPException } from 'hono/http-exception';
6
- import { z } from 'zod';
7
7
  import { sqliteTable } from 'drizzle-orm/sqlite-core';
8
8
  import { Hono } from 'hono';
9
9
 
@@ -14,6 +14,305 @@ function d1Adapter(d1) {
14
14
  batch: (queries) => db.batch(queries)
15
15
  };
16
16
  }
17
+
18
+ // src/model/types.ts
19
+ function resolveSchemaOptionKeys(opt, accessor) {
20
+ if (opt === void 0) return void 0;
21
+ if (typeof opt === "function") return opt(accessor);
22
+ return opt;
23
+ }
24
+ function buildFieldAccessor(fields) {
25
+ const acc = {};
26
+ for (const k of Object.keys(fields)) acc[k] = k;
27
+ return Object.freeze(acc);
28
+ }
29
+
30
+ // src/model/schema.ts
31
+ function computeForcedOmit(fields) {
32
+ const omit = [];
33
+ for (const [key, field] of Object.entries(fields)) {
34
+ if (field.modifiers.policy === "serverOnly") {
35
+ omit.push(key);
36
+ }
37
+ }
38
+ return omit;
39
+ }
40
+ function computeOptionalOmit(fields, target) {
41
+ const omit = [];
42
+ for (const [key, field] of Object.entries(fields)) {
43
+ const policy = field.modifiers.policy;
44
+ if (!policy) continue;
45
+ if (policy === "writeOnly" && target === "output") {
46
+ omit.push(key);
47
+ } else if (policy === "readOnly" && (target === "input-create" || target === "input-update")) {
48
+ omit.push(key);
49
+ }
50
+ }
51
+ return omit;
52
+ }
53
+ function derivePolicyOptions(fields, usage, userOpts, accessor) {
54
+ const forced = computeForcedOmit(fields);
55
+ const optional = computeOptionalOmit(fields, usage);
56
+ const acc = accessor ?? {};
57
+ const resolvedPick = resolveSchemaOptionKeys(userOpts?.pick, acc);
58
+ const resolvedOmit = resolveSchemaOptionKeys(userOpts?.omit, acc);
59
+ if (resolvedPick !== void 0) {
60
+ const partial2 = usage === "input-update" ? userOpts?.partial ?? true : userOpts?.partial;
61
+ const sanitizedPick = Array.from(resolvedPick).filter((k) => !forced.includes(k));
62
+ const combinedOmit2 = [.../* @__PURE__ */ new Set([...forced, ...resolvedOmit ?? []])];
63
+ return {
64
+ ...userOpts,
65
+ pick: sanitizedPick,
66
+ ...combinedOmit2.length > 0 ? { omit: combinedOmit2 } : {},
67
+ partial: partial2
68
+ };
69
+ }
70
+ const combinedOmit = [.../* @__PURE__ */ new Set([...forced, ...optional, ...resolvedOmit ?? []])];
71
+ const partial = usage === "input-update" ? userOpts?.partial ?? true : userOpts?.partial ?? void 0;
72
+ return {
73
+ ...userOpts ?? {},
74
+ pick: void 0,
75
+ ...combinedOmit.length > 0 ? { omit: combinedOmit } : {},
76
+ ...partial !== void 0 ? { partial } : {}
77
+ };
78
+ }
79
+ function buildBaseObject(fields) {
80
+ const shape = {};
81
+ for (const [key, field] of Object.entries(fields)) {
82
+ shape[key] = field.zodBase;
83
+ }
84
+ return z.object(shape);
85
+ }
86
+ function applySchemaOptions(baseSchema, opts, accessor) {
87
+ if (!opts) {
88
+ return baseSchema;
89
+ }
90
+ const acc = accessor ?? {};
91
+ const resolvedPick = resolveSchemaOptionKeys(opts.pick, acc);
92
+ const resolvedOmit = resolveSchemaOptionKeys(opts.omit, acc);
93
+ let schema = baseSchema;
94
+ if (resolvedPick !== void 0) {
95
+ const pickArray = Array.from(resolvedPick);
96
+ schema = schema.pick(
97
+ // biome-ignore lint/suspicious/noExplicitAny: Zod API
98
+ Object.fromEntries(pickArray.map((k) => [k, true]))
99
+ );
100
+ }
101
+ if (resolvedOmit !== void 0) {
102
+ const omitArray = Array.from(resolvedOmit);
103
+ schema = schema.omit(
104
+ // biome-ignore lint/suspicious/noExplicitAny: Zod API
105
+ Object.fromEntries(omitArray.map((k) => [k, true]))
106
+ );
107
+ }
108
+ if (opts.partial === true) {
109
+ schema = schema.partial();
110
+ }
111
+ return schema;
112
+ }
113
+
114
+ // src/openapi/generate.ts
115
+ function toOpenAPIComponent(fields, opts) {
116
+ return {
117
+ create: toOpenAPISchema(fields, "create", opts),
118
+ update: toOpenAPISchema(fields, "update", opts),
119
+ output: toOpenAPISchema(fields, "output", opts)
120
+ };
121
+ }
122
+ function toOpenAPISchema(fields, usage, opts) {
123
+ const policyUsage = usage === "create" ? "input-create" : usage === "update" ? "input-update" : "output";
124
+ const merged = derivePolicyOptions(fields, policyUsage, opts?.opts);
125
+ const schema = applySchemaOptions(buildBaseObject(fields), merged);
126
+ const shape = getZodObjectShape(schema);
127
+ const properties = {};
128
+ const required = [];
129
+ for (const [key, zodSchema] of Object.entries(shape)) {
130
+ const field = fields[key];
131
+ if (field === void 0) continue;
132
+ properties[key] = fieldToOpenAPISchema(field, opts?.strict);
133
+ if (field.modifiers.policy === "writeOnly" && (usage === "create" || usage === "update")) {
134
+ properties[key].writeOnly = true;
135
+ }
136
+ if (field.modifiers.policy === "readOnly" && usage === "output") {
137
+ properties[key].readOnly = true;
138
+ }
139
+ if (usage !== "update" && isRequiredZodSchema(zodSchema)) {
140
+ required.push(key);
141
+ }
142
+ }
143
+ return {
144
+ type: "object",
145
+ properties,
146
+ ...required.length > 0 ? { required } : {}
147
+ };
148
+ }
149
+ function fieldToOpenAPISchema(field, strict = false) {
150
+ const schema = field.kind === "json" ? zodToOpenAPISchema(field.zodBase, strict) : field.kind === "string" ? { type: "string" } : field.kind === "uuid" ? { type: "string", format: "uuid" } : field.kind === "number" ? { type: "number" } : field.kind === "integer" ? { type: "integer" } : field.kind === "boolean" ? { type: "boolean" } : field.kind === "timestamp" ? { type: "string", format: "date-time" } : {};
151
+ if (field.modifiers.format === "email") {
152
+ schema.format = "email";
153
+ }
154
+ if (field.modifiers.min !== void 0) {
155
+ if (field.kind === "string" || field.kind === "uuid") {
156
+ schema.minLength = field.modifiers.min;
157
+ } else {
158
+ schema.minimum = field.modifiers.min;
159
+ }
160
+ }
161
+ if (field.modifiers.max !== void 0) {
162
+ if (field.kind === "string" || field.kind === "uuid") {
163
+ schema.maxLength = field.modifiers.max;
164
+ } else {
165
+ schema.maximum = field.modifiers.max;
166
+ }
167
+ }
168
+ return schema;
169
+ }
170
+ function zodToOpenAPISchema(schema, strict = false) {
171
+ const def = getZodDef(schema);
172
+ if (def === void 0) return {};
173
+ const typeName = getZodTypeName(def);
174
+ if (typeName === "ZodOptional" || typeName === "optional") {
175
+ return zodToOpenAPISchema(def.innerType, strict);
176
+ }
177
+ if (typeName === "ZodDefault" || typeName === "default") {
178
+ return zodToOpenAPISchema(def.innerType, strict);
179
+ }
180
+ if (typeName === "ZodNullable" || typeName === "nullable") {
181
+ return { ...zodToOpenAPISchema(def.innerType, strict), nullable: true };
182
+ }
183
+ if (typeName === "ZodObject" || typeName === "object") {
184
+ const shape = getZodObjectShape(schema);
185
+ const properties = {};
186
+ const required = [];
187
+ for (const [key, childSchema] of Object.entries(shape)) {
188
+ properties[key] = zodToOpenAPISchema(childSchema, strict);
189
+ if (isRequiredZodSchema(childSchema)) {
190
+ required.push(key);
191
+ }
192
+ }
193
+ return {
194
+ type: "object",
195
+ properties,
196
+ ...required.length > 0 ? { required } : {}
197
+ };
198
+ }
199
+ if (typeName === "ZodString" || typeName === "string") {
200
+ return applyStringChecks({ type: "string" }, def);
201
+ }
202
+ if (typeName === "ZodNumber" || typeName === "number") {
203
+ return applyNumberChecks({ type: hasIntegerCheck(def) ? "integer" : "number" }, def);
204
+ }
205
+ if (typeName === "ZodBoolean" || typeName === "boolean") {
206
+ return { type: "boolean" };
207
+ }
208
+ if (typeName === "ZodArray" || typeName === "array") {
209
+ const itemSchema = def.element ?? def.type;
210
+ return {
211
+ type: "array",
212
+ items: zodToOpenAPISchema(itemSchema, strict)
213
+ };
214
+ }
215
+ if (typeName === "ZodRecord" || typeName === "record") {
216
+ return {
217
+ type: "object",
218
+ additionalProperties: zodToOpenAPISchema(def.valueType, strict)
219
+ };
220
+ }
221
+ if (typeName === "ZodUnknown" || typeName === "unknown") {
222
+ return {};
223
+ }
224
+ if (strict) {
225
+ throw new Error(`Unsupported Zod type for strict OpenAPI generation: ${typeName ?? "unknown"}`);
226
+ }
227
+ return {
228
+ "x-nanoka-zod-unsupported": true,
229
+ ...typeName ? { "x-nanoka-zod-type": typeName } : {}
230
+ };
231
+ }
232
+ function getZodObjectShape(schema) {
233
+ if (typeof schema === "object" && schema !== null && "shape" in schema && typeof schema.shape === "object" && schema.shape !== null) {
234
+ return schema.shape;
235
+ }
236
+ if (typeof schema === "object" && schema !== null && "_def" in schema && typeof schema._def === "object" && schema._def !== null && "shape" in schema._def) {
237
+ const shape = schema._def.shape;
238
+ return typeof shape === "function" ? shape() : shape;
239
+ }
240
+ return {};
241
+ }
242
+ function isRequiredZodSchema(schema) {
243
+ const def = getZodDef(schema);
244
+ if (def === void 0) {
245
+ return true;
246
+ }
247
+ const typeName = getZodTypeName(def);
248
+ return typeName !== "ZodOptional" && typeName !== "optional" && typeName !== "ZodDefault" && typeName !== "default";
249
+ }
250
+ function getZodDef(schema) {
251
+ if (typeof schema === "object" && schema !== null && "_def" in schema && typeof schema._def === "object" && schema._def !== null) {
252
+ return schema._def;
253
+ }
254
+ return void 0;
255
+ }
256
+ function getZodTypeName(def) {
257
+ return typeof def.typeName === "string" ? def.typeName : typeof def.type === "string" ? def.type : void 0;
258
+ }
259
+ function applyStringChecks(schema, def) {
260
+ for (const check of getZodChecks(def)) {
261
+ const checkDef = getZodCheckDef(check);
262
+ if (checkDef.kind === "email" || checkDef.format === "email") {
263
+ schema.format = "email";
264
+ }
265
+ if (checkDef.kind === "min" && typeof checkDef.value === "number") {
266
+ schema.minLength = checkDef.value;
267
+ }
268
+ if (checkDef.kind === "max" && typeof checkDef.value === "number") {
269
+ schema.maxLength = checkDef.value;
270
+ }
271
+ if (checkDef.check === "min_length" && typeof checkDef.minimum === "number") {
272
+ schema.minLength = checkDef.minimum;
273
+ }
274
+ if (checkDef.check === "max_length" && typeof checkDef.maximum === "number") {
275
+ schema.maxLength = checkDef.maximum;
276
+ }
277
+ }
278
+ return schema;
279
+ }
280
+ function applyNumberChecks(schema, def) {
281
+ for (const check of getZodChecks(def)) {
282
+ const checkDef = getZodCheckDef(check);
283
+ if (checkDef.kind === "min" && typeof checkDef.value === "number") {
284
+ schema.minimum = checkDef.value;
285
+ }
286
+ if (checkDef.kind === "max" && typeof checkDef.value === "number") {
287
+ schema.maximum = checkDef.value;
288
+ }
289
+ if ((checkDef.check === "greater_than" || checkDef.check === "number_format") && typeof checkDef.minimum === "number") {
290
+ schema.minimum = checkDef.minimum;
291
+ }
292
+ if ((checkDef.check === "less_than" || checkDef.check === "number_format") && typeof checkDef.maximum === "number") {
293
+ schema.maximum = checkDef.maximum;
294
+ }
295
+ }
296
+ return schema;
297
+ }
298
+ function hasIntegerCheck(def) {
299
+ return getZodChecks(def).some((check) => {
300
+ const checkDef = getZodCheckDef(check);
301
+ return checkDef.kind === "int" || checkDef.format === "safeint" || checkDef.format === "int32" || checkDef.format === "int64";
302
+ });
303
+ }
304
+ function getZodChecks(def) {
305
+ return Array.isArray(def.checks) ? def.checks : [];
306
+ }
307
+ function getZodCheckDef(check) {
308
+ if (typeof check !== "object" || check === null) {
309
+ return {};
310
+ }
311
+ if ("_zod" in check && typeof check._zod === "object" && check._zod !== null && "def" in check._zod && typeof check._zod.def === "object" && check._zod.def !== null) {
312
+ return check._zod.def;
313
+ }
314
+ return check;
315
+ }
17
316
  var MAX_LIMIT = 100;
18
317
  function guardLimit(limit) {
19
318
  if (!Number.isInteger(limit)) {
@@ -39,9 +338,9 @@ function guardOffset(offset) {
39
338
  return n;
40
339
  }
41
340
  function findPrimaryKey(fields) {
341
+ let pkKey;
342
+ let pkField;
42
343
  let pkCount = 0;
43
- let pkKey = null;
44
- let pkField = null;
45
344
  for (const [key, field] of Object.entries(fields)) {
46
345
  if (field.modifiers.primary === true) {
47
346
  pkCount++;
@@ -57,6 +356,9 @@ function findPrimaryKey(fields) {
57
356
  if (pkCount > 1) {
58
357
  throw new HTTPException(500, { message: "model has multiple primary keys" });
59
358
  }
359
+ if (pkKey === void 0 || pkField === void 0) {
360
+ return null;
361
+ }
60
362
  return [pkKey, pkField];
61
363
  }
62
364
  function buildWhereClause(table, fields, where) {
@@ -166,37 +468,6 @@ async function deleteImpl(adapter, table, fields, idOrWhere) {
166
468
  const result = await adapter.drizzle.delete(table).where(whereClause).returning();
167
469
  return { deleted: result.length };
168
470
  }
169
- function buildBaseObject(fields) {
170
- const shape = {};
171
- for (const [key, field] of Object.entries(fields)) {
172
- shape[key] = field.zodBase;
173
- }
174
- return z.object(shape);
175
- }
176
- function applySchemaOptions(baseSchema, opts) {
177
- if (!opts) {
178
- return baseSchema;
179
- }
180
- let schema = baseSchema;
181
- if (opts.pick !== void 0) {
182
- const pickArray = Array.from(opts.pick);
183
- schema = schema.pick(
184
- // biome-ignore lint/suspicious/noExplicitAny: Zod API
185
- Object.fromEntries(pickArray.map((k) => [k, true]))
186
- );
187
- }
188
- if (opts.omit !== void 0) {
189
- const omitArray = Array.from(opts.omit);
190
- schema = schema.omit(
191
- // biome-ignore lint/suspicious/noExplicitAny: Zod API
192
- Object.fromEntries(omitArray.map((k) => [k, true]))
193
- );
194
- }
195
- if (opts.partial === true) {
196
- schema = schema.partial();
197
- }
198
- return schema;
199
- }
200
471
  function buildTable(tableName, fields) {
201
472
  const columns = {};
202
473
  for (const [key, field] of Object.entries(fields)) {
@@ -208,19 +479,49 @@ function buildTable(tableName, fields) {
208
479
  // src/model/define.ts
209
480
  function defineModel(tableName, fields) {
210
481
  const table = buildTable(tableName, fields);
482
+ const accessor = buildFieldAccessor(fields);
211
483
  return {
212
484
  fields,
213
485
  tableName,
214
486
  table,
215
487
  schema(opts) {
216
488
  const baseSchema = buildBaseObject(fields);
217
- const schema = applySchemaOptions(baseSchema, opts);
489
+ const schema = applySchemaOptions(baseSchema, opts, accessor);
218
490
  return schema;
219
491
  },
220
- validator(target, opts, hook) {
221
- const schema = this.schema(opts);
492
+ validator(target, optsOrPreset, hook) {
493
+ let schema;
494
+ if (optsOrPreset === "create" || optsOrPreset === "update") {
495
+ schema = this.inputSchema(optsOrPreset);
496
+ } else {
497
+ schema = this.schema(optsOrPreset);
498
+ }
222
499
  return zValidator(target, schema, hook);
223
500
  },
501
+ inputSchema(usage, opts) {
502
+ const merged = derivePolicyOptions(
503
+ fields,
504
+ usage === "create" ? "input-create" : "input-update",
505
+ opts,
506
+ accessor
507
+ );
508
+ const baseSchema = buildBaseObject(fields);
509
+ return applySchemaOptions(baseSchema, merged);
510
+ },
511
+ outputSchema(opts) {
512
+ const merged = derivePolicyOptions(fields, "output", opts, accessor);
513
+ const baseSchema = buildBaseObject(fields);
514
+ return applySchemaOptions(baseSchema, merged);
515
+ },
516
+ toResponse(row) {
517
+ return this.outputSchema().parse(row);
518
+ },
519
+ toOpenAPIComponent() {
520
+ return toOpenAPIComponent(fields);
521
+ },
522
+ toOpenAPISchema(usage) {
523
+ return toOpenAPISchema(fields, usage);
524
+ },
224
525
  findMany(adapter, options) {
225
526
  return findManyImpl(adapter, table, fields, options);
226
527
  },
@@ -238,13 +539,91 @@ function defineModel(tableName, fields) {
238
539
  }
239
540
  };
240
541
  }
542
+
543
+ // src/openapi/route.ts
544
+ function honoPathToOpenAPI(path) {
545
+ return path.replace(/:([^/]+)/g, "{$1}");
546
+ }
547
+ function buildOpenAPIDocument(routes, options) {
548
+ const paths = {};
549
+ for (const route of routes) {
550
+ const openAPIPath = honoPathToOpenAPI(route.path);
551
+ if (!paths[openAPIPath]) {
552
+ paths[openAPIPath] = {};
553
+ }
554
+ const operation = {};
555
+ if (route.operationId) operation.operationId = route.operationId;
556
+ if (route.summary) operation.summary = route.summary;
557
+ if (route.description) operation.description = route.description;
558
+ if (route.tags) operation.tags = route.tags;
559
+ const parameters = [];
560
+ const pathParams = [...route.path.matchAll(/:([^/]+)/g)].map((m) => m[1]);
561
+ for (const param of pathParams) {
562
+ const alreadyDefined = route.params?.some((p) => p.name === param && p.in === "path");
563
+ if (!alreadyDefined) {
564
+ parameters.push({ name: param, in: "path", required: true, schema: { type: "string" } });
565
+ }
566
+ }
567
+ if (route.params) {
568
+ parameters.push(...route.params);
569
+ }
570
+ if (parameters.length > 0) operation.parameters = parameters;
571
+ if (route.requestBody) operation.requestBody = route.requestBody;
572
+ operation.responses = route.responses;
573
+ paths[openAPIPath][route.method] = operation;
574
+ }
575
+ const doc = {
576
+ openapi: "3.1.0",
577
+ info: options.info,
578
+ ...options.servers ? { servers: options.servers } : {},
579
+ paths
580
+ };
581
+ return doc;
582
+ }
583
+
584
+ // src/openapi/swagger-ui.ts
585
+ function escapeHtml(str) {
586
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
587
+ }
588
+ function swaggerUI(options) {
589
+ const title = escapeHtml(options.title ?? "API Documentation");
590
+ const html = `<!DOCTYPE html>
591
+ <html lang="en">
592
+ <head>
593
+ <meta charset="UTF-8" />
594
+ <title>${title}</title>
595
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
596
+ </head>
597
+ <body>
598
+ <div id="swagger-ui"></div>
599
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
600
+ <script>
601
+ SwaggerUIBundle({
602
+ url: ${JSON.stringify(options.url)},
603
+ dom_id: '#swagger-ui',
604
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
605
+ layout: 'BaseLayout',
606
+ })
607
+ </script>
608
+ </body>
609
+ </html>`;
610
+ return async (c) => {
611
+ return c.html(html);
612
+ };
613
+ }
241
614
  function nanoka(adapter) {
242
615
  const app = new Hono();
616
+ const openapiRoutes = [];
243
617
  Object.defineProperty(app, "db", {
244
618
  get: () => adapter.drizzle,
245
619
  enumerable: true
246
620
  });
247
621
  app.batch = ((queries) => adapter.batch(queries));
622
+ app.openapi = (metadata) => {
623
+ openapiRoutes.push(metadata);
624
+ return app;
625
+ };
626
+ app.generateOpenAPISpec = (options) => buildOpenAPIDocument(openapiRoutes, options);
248
627
  app.model = function model(name, fields) {
249
628
  const inner = defineModel(name, fields);
250
629
  return {
@@ -252,7 +631,14 @@ function nanoka(adapter) {
252
631
  tableName: inner.tableName,
253
632
  table: inner.table,
254
633
  schema: (opts) => inner.schema(opts),
255
- validator: (target, opts, hook) => inner.validator(target, opts, hook),
634
+ validator: (...args) => {
635
+ return inner.validator(...args);
636
+ },
637
+ inputSchema: (usage, opts) => inner.inputSchema(usage, opts),
638
+ outputSchema: (opts) => inner.outputSchema(opts),
639
+ toResponse: (row) => inner.toResponse(row),
640
+ toOpenAPIComponent: () => inner.toOpenAPIComponent(),
641
+ toOpenAPISchema: (usage) => inner.toOpenAPISchema(usage),
256
642
  findMany: (options) => inner.findMany(adapter, options),
257
643
  findOne: (idOrWhere) => inner.findOne(adapter, idOrWhere),
258
644
  create: (data) => inner.create(adapter, data),
@@ -263,6 +649,6 @@ function nanoka(adapter) {
263
649
  return app;
264
650
  }
265
651
 
266
- export { d1Adapter, defineModel, nanoka };
652
+ export { d1Adapter, defineModel, nanoka, swaggerUI };
267
653
  //# sourceMappingURL=index.js.map
268
654
  //# sourceMappingURL=index.js.map