@nanokajs/core 0.1.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/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/chunk-F5OBRXXY.js +571 -0
- package/dist/chunk-F5OBRXXY.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +181 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -0
- package/dist/factories-CDsk1Hdt.d.ts +402 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +268 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eiichiro Iriguchi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/A-1ro/nanoka/main/assets/images/nanoka-logo.png" alt="nanoka" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# nanoka
|
|
6
|
+
|
|
7
|
+
> Thin wrapper over Hono + Drizzle + Zod for Cloudflare Workers + D1.
|
|
8
|
+
> 80% automatic, 20% explicit.
|
|
9
|
+
|
|
10
|
+
## What is this
|
|
11
|
+
|
|
12
|
+
Nanoka is a lightweight framework that bridges model definition with DB schema, TypeScript types, and validation. It places the model at the center and derives schema, types, and base validation from it—but keeps API validation as an intentional, explicit adaptation layer, because DB shape and API shape diverge (e.g., `passwordHash` exists in the DB but must not appear in responses).
|
|
13
|
+
|
|
14
|
+
The core idea: stop gluing together Hono + Drizzle + Zod manually. Define a model once, get DB schema, types, and validators automatically. Then customize API behavior explicitly for each route.
|
|
15
|
+
|
|
16
|
+
Targets **Cloudflare Workers + D1 (SQLite)** as first-class. Hono-compatible routing, escape hatch to raw Drizzle, zero magic.
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
Phase 1 (MVP). Experimental. Expect breaking changes until v1.0.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @nanokajs/core hono drizzle-orm zod
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Peer dependencies: `hono ^4.0.0`, `drizzle-orm ^0.45.0`, `zod ^3.23.0`, `@cloudflare/workers-types ^4.20240925.0` (for Workers).
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
### 3 commands to a working API
|
|
33
|
+
|
|
34
|
+
1. **Define your model**
|
|
35
|
+
|
|
36
|
+
`src/models/user.ts`:
|
|
37
|
+
```ts
|
|
38
|
+
import { t } from '@nanokajs/core'
|
|
39
|
+
|
|
40
|
+
export const userTableName = 'users'
|
|
41
|
+
export const userFields = {
|
|
42
|
+
id: t.uuid().primary(),
|
|
43
|
+
name: t.string(),
|
|
44
|
+
email: t.string().email(),
|
|
45
|
+
passwordHash: t.string(),
|
|
46
|
+
createdAt: t.timestamp().default(() => new Date()),
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. **Generate Drizzle schema**
|
|
51
|
+
|
|
52
|
+
Create `nanoka.config.ts` in your project root:
|
|
53
|
+
```ts
|
|
54
|
+
import { defineConfig } from '@nanokajs/core/config'
|
|
55
|
+
import { userTableName, userFields } from './src/models/user'
|
|
56
|
+
|
|
57
|
+
export default defineConfig({
|
|
58
|
+
models: [
|
|
59
|
+
{ name: userTableName, fields: userFields },
|
|
60
|
+
],
|
|
61
|
+
output: './drizzle/schema.ts',
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then:
|
|
66
|
+
```bash
|
|
67
|
+
npx nanoka generate
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
✓ Creates `drizzle/schema.ts` from your models.
|
|
71
|
+
|
|
72
|
+
3. **Apply migrations and deploy**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx drizzle-kit generate
|
|
76
|
+
npx wrangler d1 migrations apply <DATABASE> --local
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
✓ SQL migrations generated from Drizzle schema. Ready to deploy.
|
|
80
|
+
|
|
81
|
+
### Minimal example (model + 1 route)
|
|
82
|
+
|
|
83
|
+
`src/index.ts`:
|
|
84
|
+
```ts
|
|
85
|
+
import { d1Adapter, nanoka } from '@nanokajs/core'
|
|
86
|
+
import { userFields, userTableName } from './src/models/user'
|
|
87
|
+
|
|
88
|
+
export interface Env {
|
|
89
|
+
DB: D1Database
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default {
|
|
93
|
+
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
|
|
94
|
+
const app = nanoka(d1Adapter(env.DB))
|
|
95
|
+
const User = app.model(userTableName, userFields)
|
|
96
|
+
|
|
97
|
+
app.get('/users/:id', async (c) => {
|
|
98
|
+
const id = c.req.param('id')
|
|
99
|
+
const user = await User.findOne(id)
|
|
100
|
+
if (!user) return c.notFound()
|
|
101
|
+
return c.json(user)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
app.post(
|
|
105
|
+
'/users',
|
|
106
|
+
User.validator('json', { omit: ['id', 'passwordHash', 'createdAt'] }),
|
|
107
|
+
async (c) => {
|
|
108
|
+
const body = c.req.valid('json')
|
|
109
|
+
const user = await User.create({
|
|
110
|
+
...body,
|
|
111
|
+
id: crypto.randomUUID(),
|
|
112
|
+
passwordHash: 'hashed_value_here', // use bcrypt, argon2, etc. in production
|
|
113
|
+
createdAt: new Date(),
|
|
114
|
+
})
|
|
115
|
+
return c.json(user, 201)
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return app.fetch(req, env, ctx)
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Full working example
|
|
125
|
+
|
|
126
|
+
Complete CRUD with curl tests, deployment steps, and troubleshooting:
|
|
127
|
+
[examples/basic/README.md](../../examples/basic/README.md)
|
|
128
|
+
|
|
129
|
+
## How it fits with drizzle-kit and wrangler
|
|
130
|
+
|
|
131
|
+
### Migration flow
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
User model definition
|
|
135
|
+
(userFields + userTableName)
|
|
136
|
+
|
|
|
137
|
+
v
|
|
138
|
+
nanoka generate
|
|
139
|
+
|
|
|
140
|
+
v
|
|
141
|
+
drizzle/schema.ts (Drizzle schema code)
|
|
142
|
+
|
|
|
143
|
+
v
|
|
144
|
+
drizzle-kit generate
|
|
145
|
+
|
|
|
146
|
+
v
|
|
147
|
+
drizzle/migrations/*.sql (SQL migrations)
|
|
148
|
+
|
|
|
149
|
+
v
|
|
150
|
+
wrangler d1 migrations apply
|
|
151
|
+
|
|
|
152
|
+
v
|
|
153
|
+
D1 Database
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Nanoka generates Drizzle schema code only.** Nanoka does NOT generate SQL, does not compute diffs, does not apply migrations. That responsibility stays with `drizzle-kit` (diff + SQL) and `wrangler` (apply).
|
|
157
|
+
|
|
158
|
+
This design keeps Nanoka thin and lets the stable Drizzle ecosystem handle migration machinery. You own the SQL files; migrations are human-reviewable and version-controlled.
|
|
159
|
+
|
|
160
|
+
## Core API
|
|
161
|
+
|
|
162
|
+
### Model definition with `t`
|
|
163
|
+
|
|
164
|
+
Fields are built with the `t` builder:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { t } from '@nanokajs/core'
|
|
168
|
+
|
|
169
|
+
const fields = {
|
|
170
|
+
id: t.uuid().primary(),
|
|
171
|
+
name: t.string(),
|
|
172
|
+
email: t.string().email(),
|
|
173
|
+
passwordHash: t.string(),
|
|
174
|
+
active: t.boolean().default(() => true),
|
|
175
|
+
age: t.integer().min(0).max(150),
|
|
176
|
+
metadata: t.json(), // { key: string, value: unknown }[]
|
|
177
|
+
createdAt: t.timestamp().default(() => new Date()),
|
|
178
|
+
updatedAt: t.timestamp().default(() => new Date()),
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Supported field types:
|
|
183
|
+
- `t.string()` / `t.uuid()`
|
|
184
|
+
- `t.integer()` / `t.number()`
|
|
185
|
+
- `t.boolean()`
|
|
186
|
+
- `t.timestamp()`
|
|
187
|
+
- `t.json()`
|
|
188
|
+
|
|
189
|
+
Modifiers:
|
|
190
|
+
- `.primary()` — primary key (required, one per model)
|
|
191
|
+
- `.unique()` — unique constraint
|
|
192
|
+
- `.optional()` — nullable
|
|
193
|
+
- `.default(fn)` — default value (function or constant)
|
|
194
|
+
- `.min(n)` / `.max(n)` — numeric / string length bounds
|
|
195
|
+
- `.email()` — email validation (string modifier: `t.string().email()`)
|
|
196
|
+
|
|
197
|
+
### `schema()` vs `validator()` (two methods, not one)
|
|
198
|
+
|
|
199
|
+
This intentional separation keeps validation library swappable for Phase 2.
|
|
200
|
+
|
|
201
|
+
**`schema(opts)` — pure Zod schema, standalone**
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const CreateSchema = User.schema({ omit: ['id', 'passwordHash', 'createdAt'] })
|
|
205
|
+
const parsed = CreateSchema.parse(data) // use anywhere Zod is needed
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**`validator(target, opts, hook?)` — Hono validator**
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
app.post('/users', User.validator('json', { omit: ['id', 'passwordHash'] }), handler)
|
|
212
|
+
// or with error hook:
|
|
213
|
+
app.post('/users', User.validator('json', opts, (result, c) => {
|
|
214
|
+
if (!result.success) return c.json({ error: 'Invalid request' }, 400)
|
|
215
|
+
}), handler)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The `hook` parameter is optional. If you omit it, `@hono/zod-validator` returns issues in the response (see Error handling section for hardening).
|
|
219
|
+
|
|
220
|
+
Options apply to both:
|
|
221
|
+
- `pick: ['name', 'email']` — include only these fields
|
|
222
|
+
- `omit: ['passwordHash']` — exclude these fields
|
|
223
|
+
- `partial: true` — all fields optional (good for PATCH)
|
|
224
|
+
|
|
225
|
+
### `findMany` requires `limit` (type-safe pagination)
|
|
226
|
+
|
|
227
|
+
`limit` is a required parameter—omitting it is a **compile error**:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// @ts-expect-error Missing required property 'limit'
|
|
231
|
+
const users = await User.findMany()
|
|
232
|
+
|
|
233
|
+
// OK
|
|
234
|
+
const users = await User.findMany({ limit: 20 })
|
|
235
|
+
const users = await User.findMany({ limit: 20, offset: 10 })
|
|
236
|
+
const users = await User.findMany({ limit: 20, orderBy: 'id' })
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This prevents accidental unbounded queries. Pagination shape: `{ limit, offset?, orderBy? }`.
|
|
240
|
+
|
|
241
|
+
## Escape hatch: raw Drizzle and D1 batch
|
|
242
|
+
|
|
243
|
+
### `app.db` — use raw Drizzle when needed
|
|
244
|
+
|
|
245
|
+
When the typed API doesn't fit, drop down to `app.db` (raw Drizzle):
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { eq } from 'drizzle-orm'
|
|
249
|
+
|
|
250
|
+
// Raw select with explicit types
|
|
251
|
+
const users = await app.db
|
|
252
|
+
.select()
|
|
253
|
+
.from(User.table)
|
|
254
|
+
.where(eq(User.table.email, 'alice@example.com'))
|
|
255
|
+
|
|
256
|
+
// Complex joins, subqueries, raw SQL
|
|
257
|
+
const result = await app.db
|
|
258
|
+
.select()
|
|
259
|
+
.from(User.table)
|
|
260
|
+
.innerJoin(Post.table, eq(User.table.id, Post.table.userId))
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`User.table` is the underlying Drizzle table. Combine with `eq()`, `lt()`, `and()`, `or()` from `drizzle-orm` for complex conditions. SQL injection is prevented by Drizzle's parametrized bindings.
|
|
264
|
+
|
|
265
|
+
### `app.batch()` — D1 batch API directly
|
|
266
|
+
|
|
267
|
+
For transactions or batched queries, use `app.batch()` (D1's native batch):
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const results = await app.batch([
|
|
271
|
+
app.db.insert(User.table).values({ ... }),
|
|
272
|
+
app.db.update(User.table).set({ ... }).where(...),
|
|
273
|
+
])
|
|
274
|
+
|
|
275
|
+
// D1 batch returns results in order
|
|
276
|
+
const [insertResult, updateResult] = results
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
No custom transaction abstraction. Nanoka exposes D1's `batch()` directly. Isolation level and rollback behavior match D1 docs.
|
|
280
|
+
|
|
281
|
+
## Error handling
|
|
282
|
+
|
|
283
|
+
### `HTTPException` (Hono standard)
|
|
284
|
+
|
|
285
|
+
Nanoka uses Hono's `HTTPException` for errors:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { HTTPException } from 'hono/http-exception'
|
|
289
|
+
|
|
290
|
+
app.get('/users/:id', async (c) => {
|
|
291
|
+
const user = await User.findOne(c.req.param('id'))
|
|
292
|
+
if (!user) throw new HTTPException(404, { message: 'Not found' })
|
|
293
|
+
return c.json(user)
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Hardening Zod validator errors
|
|
298
|
+
|
|
299
|
+
By default, `@hono/zod-validator` returns the full `issues[]` array in the response, which leaks your API schema shape (field names, types, constraints) to attackers during reconnaissance. Default zod-validator behavior leaks the API schema shape via `issues[]`. Replace with a fixed string in production.
|
|
300
|
+
|
|
301
|
+
**Option 1: validator hook (recommended)**
|
|
302
|
+
|
|
303
|
+
Pass a hook as the third argument to `User.validator()`. The hook receives the validation result and the Hono context. When you return a response from the hook, it replaces the default behavior:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
app.post(
|
|
307
|
+
'/users',
|
|
308
|
+
User.validator('json', { omit: ['passwordHash'] }, (result, c) => {
|
|
309
|
+
if (!result.success) return c.json({ error: 'Invalid request' }, 400)
|
|
310
|
+
}),
|
|
311
|
+
handler
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Option 2: `app.onError` (for non-validator paths)**
|
|
316
|
+
|
|
317
|
+
`@hono/zod-validator` does not throw on validation failure — it calls `c.json(result, 400)` directly, so `app.onError` does not receive validator failures. Use `app.onError` for cases where you call `User.schema().parse(data)` directly or write middleware that throws `ZodError`:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { ZodError } from 'zod'
|
|
321
|
+
|
|
322
|
+
app.onError((err, c) => {
|
|
323
|
+
if (err instanceof ZodError) {
|
|
324
|
+
return c.json({ error: 'Invalid request' }, 400)
|
|
325
|
+
}
|
|
326
|
+
// ... handle other errors
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Note: this handler will not be called for validation failures originating from `User.validator()`. Use Option 1 for those.
|
|
331
|
+
|
|
332
|
+
## Phase 1 scope: what is NOT included
|
|
333
|
+
|
|
334
|
+
Nanoka Phase 1 is intentionally minimal. These features are Phase 2 or later:
|
|
335
|
+
|
|
336
|
+
- **Relations** (`hasMany()`, `belongsTo()`, lazy loading) — Phase 2. Use raw Drizzle for joins.
|
|
337
|
+
- **Field accessor API** (`User.schema({ pick: f => [f.name, f.email] })`, `User.where(f => eq(f.email, x))`) — Phase 2. Phase 1 uses string arrays and object-form `where`. When Phase 2 lands, the `f` object will be `as const`, with zero runtime cost.
|
|
338
|
+
- **OpenAPI generation** — Phase 2 or 3.
|
|
339
|
+
- **Turso / libSQL adapters** — Phase 2. Nanoka is designed adapter-first; additional adapters will follow.
|
|
340
|
+
- **CLI scaffolder** (`create-nanoka-app`) — Phase 3.
|
|
341
|
+
- **Auth, full-stack React, complex query DSL** — explicitly out of scope at every phase.
|
|
342
|
+
|
|
343
|
+
For relations and complex joins in Phase 1, use raw Drizzle via `app.db`. The escape hatch is always open.
|
|
344
|
+
|
|
345
|
+
## Workspace structure (for contributors)
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
packages/nanoka/ # Library
|
|
349
|
+
src/
|
|
350
|
+
field/ # DSL builder (t)
|
|
351
|
+
model/ # Model definition, schema(), validator()
|
|
352
|
+
router/ # nanoka() router, CRUD methods
|
|
353
|
+
adapter/ # Adapter interface, d1Adapter()
|
|
354
|
+
cli/ # CLI entry point, nanoka generate
|
|
355
|
+
codegen/ # Schema code generator
|
|
356
|
+
dist/ # Built ESM + types (not in repo, generated by tsup)
|
|
357
|
+
__tests__/ # Integration tests
|
|
358
|
+
README.md # This file
|
|
359
|
+
LICENSE # MIT
|
|
360
|
+
package.json
|
|
361
|
+
tsup.config.ts
|
|
362
|
+
vitest.config.ts
|
|
363
|
+
vitest.node.config.ts
|
|
364
|
+
tsconfig.json
|
|
365
|
+
|
|
366
|
+
examples/basic/ # Cloudflare Workers example
|
|
367
|
+
src/models/user.ts
|
|
368
|
+
src/index.ts
|
|
369
|
+
drizzle/schema.ts # Generated by nanoka generate
|
|
370
|
+
drizzle/migrations/ # Generated by drizzle-kit generate
|
|
371
|
+
README.md
|
|
372
|
+
wrangler.toml
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Release checklist (for maintainers)
|
|
376
|
+
|
|
377
|
+
Before running `pnpm publish`:
|
|
378
|
+
|
|
379
|
+
1. **Run `pnpm publish --dry-run` and verify tarball contents**
|
|
380
|
+
```bash
|
|
381
|
+
pnpm -C packages/nanoka publish --dry-run
|
|
382
|
+
```
|
|
383
|
+
Expected in Tarball Contents:
|
|
384
|
+
- `dist/index.js`, `dist/index.d.ts`, `dist/*.js`, `dist/*.d.ts`
|
|
385
|
+
- `LICENSE`
|
|
386
|
+
- `package.json`, `README.md`
|
|
387
|
+
|
|
388
|
+
NOT included (confirm absence):
|
|
389
|
+
- `src/`, `__tests__/`, `.test.ts`, `.spec.ts`
|
|
390
|
+
- `tsconfig.json`, `vitest.config.ts`, `tsup.config.ts`, `scripts/`
|
|
391
|
+
- `node_modules/`
|
|
392
|
+
|
|
393
|
+
Note: `../../examples/basic` is a relative link in this README. It resolves correctly on GitHub but not on the npm registry. The link is kept for GitHub usability.
|
|
394
|
+
|
|
395
|
+
2. **Verify publish access**
|
|
396
|
+
```bash
|
|
397
|
+
npm view @nanokajs/core 2>&1 | grep E404
|
|
398
|
+
```
|
|
399
|
+
Confirm E404 (not yet published). Confirm that the `nanokajs` npm org has been created and you have publish rights to `@nanokajs/core`.
|
|
400
|
+
|
|
401
|
+
3. **Publish** (prepublishOnly hook will run build + test + typecheck again)
|
|
402
|
+
```bash
|
|
403
|
+
pnpm -C packages/nanoka publish
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT — see LICENSE file.
|