semola 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -13
- package/dist/index-BhGNDjPq.d.mts +13 -0
- package/dist/index-DxSbeGP-.d.cts +13 -0
- package/dist/lib/api/index.cjs +522 -4
- package/dist/lib/api/index.d.cts +270 -4
- package/dist/lib/api/index.d.mts +270 -4
- package/dist/lib/api/index.mjs +520 -2
- package/dist/lib/cache/index.d.cts +19 -7
- package/dist/lib/cache/index.d.mts +19 -7
- package/dist/lib/cache/index.mjs +0 -2
- package/dist/lib/cron/index.cjs +470 -11
- package/dist/lib/cron/index.d.cts +112 -5
- package/dist/lib/cron/index.d.mts +112 -5
- package/dist/lib/cron/index.mjs +461 -12
- package/dist/lib/errors/index.d.cts +2 -13
- package/dist/lib/errors/index.d.mts +2 -13
- package/dist/lib/errors/index.mjs +0 -2
- package/dist/lib/i18n/index.cjs +6 -1
- package/dist/lib/i18n/index.d.cts +12 -4
- package/dist/lib/i18n/index.d.mts +12 -4
- package/dist/lib/i18n/index.mjs +6 -3
- package/dist/lib/logging/index.cjs +387 -0
- package/dist/lib/logging/index.d.cts +108 -0
- package/dist/lib/logging/index.d.mts +108 -0
- package/dist/lib/logging/index.mjs +374 -0
- package/dist/lib/policy/index.cjs +206 -20
- package/dist/lib/policy/index.d.cts +61 -5
- package/dist/lib/policy/index.d.mts +61 -5
- package/dist/lib/policy/index.mjs +187 -3
- package/dist/lib/prompts/index.cjs +374 -14
- package/dist/lib/prompts/index.d.cts +77 -12
- package/dist/lib/prompts/index.d.mts +77 -12
- package/dist/lib/prompts/index.mjs +362 -4
- package/dist/lib/pubsub/index.cjs +82 -13
- package/dist/lib/pubsub/index.d.cts +23 -9
- package/dist/lib/pubsub/index.d.mts +23 -9
- package/dist/lib/pubsub/index.mjs +82 -15
- package/dist/lib/queue/index.d.cts +46 -4
- package/dist/lib/queue/index.d.mts +46 -4
- package/dist/lib/queue/index.mjs +0 -2
- package/dist/lib/workflow/index.cjs +534 -0
- package/dist/lib/workflow/index.d.cts +85 -0
- package/dist/lib/workflow/index.d.mts +85 -0
- package/dist/lib/workflow/index.mjs +533 -0
- package/package.json +29 -3
- package/dist/api/core/index.cjs +0 -206
- package/dist/api/core/index.d.cts +0 -21
- package/dist/api/core/index.d.cts.map +0 -1
- package/dist/api/core/index.d.mts +0 -21
- package/dist/api/core/index.d.mts.map +0 -1
- package/dist/api/core/index.mjs +0 -208
- package/dist/api/core/index.mjs.map +0 -1
- package/dist/api/core/types.d.cts +0 -107
- package/dist/api/core/types.d.cts.map +0 -1
- package/dist/api/core/types.d.mts +0 -107
- package/dist/api/core/types.d.mts.map +0 -1
- package/dist/api/middleware/index.cjs +0 -8
- package/dist/api/middleware/index.d.cts +0 -11
- package/dist/api/middleware/index.d.cts.map +0 -1
- package/dist/api/middleware/index.d.mts +0 -11
- package/dist/api/middleware/index.d.mts.map +0 -1
- package/dist/api/middleware/index.mjs +0 -10
- package/dist/api/middleware/index.mjs.map +0 -1
- package/dist/api/middleware/types.d.cts +0 -16
- package/dist/api/middleware/types.d.cts.map +0 -1
- package/dist/api/middleware/types.d.mts +0 -16
- package/dist/api/middleware/types.d.mts.map +0 -1
- package/dist/api/openapi/index.cjs +0 -254
- package/dist/api/openapi/index.mjs +0 -256
- package/dist/api/openapi/index.mjs.map +0 -1
- package/dist/api/openapi/types.d.cts +0 -60
- package/dist/api/openapi/types.d.cts.map +0 -1
- package/dist/api/openapi/types.d.mts +0 -60
- package/dist/api/openapi/types.d.mts.map +0 -1
- package/dist/api/validation/index.cjs +0 -64
- package/dist/api/validation/index.mjs +0 -61
- package/dist/api/validation/index.mjs.map +0 -1
- package/dist/cache/types.d.cts +0 -17
- package/dist/cache/types.d.cts.map +0 -1
- package/dist/cache/types.d.mts +0 -17
- package/dist/cache/types.d.mts.map +0 -1
- package/dist/cron/scanner.cjs +0 -237
- package/dist/cron/scanner.mjs +0 -238
- package/dist/cron/scanner.mjs.map +0 -1
- package/dist/cron/types.d.cts +0 -11
- package/dist/cron/types.d.cts.map +0 -1
- package/dist/cron/types.d.mts +0 -11
- package/dist/cron/types.d.mts.map +0 -1
- package/dist/errors/types.d.cts +0 -5
- package/dist/errors/types.d.cts.map +0 -1
- package/dist/errors/types.d.mts +0 -5
- package/dist/errors/types.d.mts.map +0 -1
- package/dist/i18n/types.d.cts +0 -13
- package/dist/i18n/types.d.cts.map +0 -1
- package/dist/i18n/types.d.mts +0 -13
- package/dist/i18n/types.d.mts.map +0 -1
- package/dist/lib/cache/index.d.cts.map +0 -1
- package/dist/lib/cache/index.d.mts.map +0 -1
- package/dist/lib/cache/index.mjs.map +0 -1
- package/dist/lib/cron/index.d.cts.map +0 -1
- package/dist/lib/cron/index.d.mts.map +0 -1
- package/dist/lib/cron/index.mjs.map +0 -1
- package/dist/lib/errors/index.d.cts.map +0 -1
- package/dist/lib/errors/index.d.mts.map +0 -1
- package/dist/lib/errors/index.mjs.map +0 -1
- package/dist/lib/i18n/index.d.cts.map +0 -1
- package/dist/lib/i18n/index.d.mts.map +0 -1
- package/dist/lib/i18n/index.mjs.map +0 -1
- package/dist/lib/policy/index.d.cts.map +0 -1
- package/dist/lib/policy/index.d.mts.map +0 -1
- package/dist/lib/policy/index.mjs.map +0 -1
- package/dist/lib/prompts/index.d.cts.map +0 -1
- package/dist/lib/prompts/index.d.mts.map +0 -1
- package/dist/lib/prompts/index.mjs.map +0 -1
- package/dist/lib/pubsub/index.d.cts.map +0 -1
- package/dist/lib/pubsub/index.d.mts.map +0 -1
- package/dist/lib/pubsub/index.mjs.map +0 -1
- package/dist/lib/queue/index.d.cts.map +0 -1
- package/dist/lib/queue/index.d.mts.map +0 -1
- package/dist/lib/queue/index.mjs.map +0 -1
- package/dist/node_modules/@standard-schema/spec/dist/index.d.cts +0 -80
- package/dist/node_modules/@standard-schema/spec/dist/index.d.cts.map +0 -1
- package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +0 -80
- package/dist/node_modules/@standard-schema/spec/dist/index.d.mts.map +0 -1
- package/dist/policy/helpers.cjs +0 -206
- package/dist/policy/helpers.d.cts +0 -50
- package/dist/policy/helpers.d.cts.map +0 -1
- package/dist/policy/helpers.d.mts +0 -50
- package/dist/policy/helpers.d.mts.map +0 -1
- package/dist/policy/helpers.mjs +0 -190
- package/dist/policy/helpers.mjs.map +0 -1
- package/dist/policy/types.d.cts +0 -16
- package/dist/policy/types.d.cts.map +0 -1
- package/dist/policy/types.d.mts +0 -16
- package/dist/policy/types.d.mts.map +0 -1
- package/dist/prompts/core/keys.cjs +0 -165
- package/dist/prompts/core/keys.mjs +0 -167
- package/dist/prompts/core/keys.mjs.map +0 -1
- package/dist/prompts/core/runtime.cjs +0 -104
- package/dist/prompts/core/runtime.mjs +0 -106
- package/dist/prompts/core/runtime.mjs.map +0 -1
- package/dist/prompts/core/session.cjs +0 -98
- package/dist/prompts/core/session.mjs +0 -100
- package/dist/prompts/core/session.mjs.map +0 -1
- package/dist/prompts/core/types.d.cts +0 -21
- package/dist/prompts/core/types.d.cts.map +0 -1
- package/dist/prompts/core/types.d.mts +0 -21
- package/dist/prompts/core/types.d.mts.map +0 -1
- package/dist/prompts/types.d.cts +0 -52
- package/dist/prompts/types.d.cts.map +0 -1
- package/dist/prompts/types.d.mts +0 -52
- package/dist/prompts/types.d.mts.map +0 -1
- package/dist/pubsub/types.d.cts +0 -10
- package/dist/pubsub/types.d.cts.map +0 -1
- package/dist/pubsub/types.d.mts +0 -10
- package/dist/pubsub/types.d.mts.map +0 -1
- package/dist/queue/types.d.cts +0 -47
- package/dist/queue/types.d.cts.map +0 -1
- package/dist/queue/types.d.mts +0 -47
- package/dist/queue/types.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -18,18 +18,20 @@ Type-safe APIs, Redis queues, pub/sub, i18n, caching & auth with tree-shakeable
|
|
|
18
18
|
|
|
19
19
|
## ✨ Features
|
|
20
20
|
|
|
21
|
-
| Module | Description | Import
|
|
22
|
-
| -------------------- | ------------------------------------------------------ |
|
|
23
|
-
| **🚀 API Framework** | Type-safe REST API with OpenAPI & Bun-native routing | `semola/api`
|
|
24
|
-
| **📬 Queue** | Redis-backed job queue with timeouts & concurrency | `semola/queue`
|
|
25
|
-
| **📡 PubSub** | Type-safe Redis pub/sub for real-time messaging | `semola/pubsub`
|
|
26
|
-
| **🔐 Policy** | Policy-based authorization with type-safe guards | `semola/policy`
|
|
27
|
-
| **🌍 i18n** | Compile-time validated internationalization | `semola/i18n`
|
|
28
|
-
| **💾 Cache** | Redis cache wrapper with TTL & automatic serialization | `semola/cache`
|
|
29
|
-
| **⏰ Cron** | In-memory cron scheduler for periodic task execution | `semola/cron`
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
21
|
+
| Module | Description | Import |
|
|
22
|
+
| -------------------- | ------------------------------------------------------ | ----------------- |
|
|
23
|
+
| **🚀 API Framework** | Type-safe REST API with OpenAPI & Bun-native routing | `semola/api` |
|
|
24
|
+
| **📬 Queue** | Redis-backed job queue with timeouts & concurrency | `semola/queue` |
|
|
25
|
+
| **📡 PubSub** | Type-safe Redis pub/sub for real-time messaging | `semola/pubsub` |
|
|
26
|
+
| **🔐 Policy** | Policy-based authorization with type-safe guards | `semola/policy` |
|
|
27
|
+
| **🌍 i18n** | Compile-time validated internationalization | `semola/i18n` |
|
|
28
|
+
| **💾 Cache** | Redis cache wrapper with TTL & automatic serialization | `semola/cache` |
|
|
29
|
+
| **⏰ Cron** | In-memory cron scheduler for periodic task execution | `semola/cron` |
|
|
30
|
+
| **🔁 Workflow** | Durable resumable workflows with step persistence | `semola/workflow` |
|
|
31
|
+
| **⚠️ Errors** | Result-based error handling without try/catch | `semola/errors` |
|
|
32
|
+
| **📃 Logging** | A simple logging utility | `semola/logging` |
|
|
33
|
+
| **⌨️ Prompts** | Interactive zero-dependency CLI prompts | `semola/prompts` |
|
|
34
|
+
| **🗄️ ORM** | Type-safe data layer with query APIs + migrations | `semola/orm` |
|
|
33
35
|
|
|
34
36
|
---
|
|
35
37
|
|
|
@@ -84,6 +86,14 @@ if (error) {
|
|
|
84
86
|
console.log("Success:", data);
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
`mightThrow` and `mightThrowSync` default their error type to `Error`. If a promise or function can reject or throw non-Error values, pass a custom error generic.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const [customError] = await mightThrow<never, { code: string }>(
|
|
93
|
+
Promise.reject({ code: "RATE_LIMITED" }),
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
87
97
|
### Process Background Jobs
|
|
88
98
|
|
|
89
99
|
```typescript
|
|
@@ -112,12 +122,16 @@ const pubsub = new PubSub({
|
|
|
112
122
|
});
|
|
113
123
|
|
|
114
124
|
// Subscribe to messages
|
|
115
|
-
await pubsub.subscribe((message) => {
|
|
125
|
+
const [, unsubscribeHandler] = await pubsub.subscribe((message) => {
|
|
116
126
|
console.log("Received:", message);
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
// Publish a message
|
|
120
130
|
await pubsub.publish({ userId: 123, text: "New alert!" });
|
|
131
|
+
|
|
132
|
+
if (unsubscribeHandler) {
|
|
133
|
+
await unsubscribeHandler();
|
|
134
|
+
}
|
|
121
135
|
```
|
|
122
136
|
|
|
123
137
|
### Cache Data with TTL
|
|
@@ -155,6 +169,65 @@ const cleanup = new Cron({
|
|
|
155
169
|
cleanup.start();
|
|
156
170
|
```
|
|
157
171
|
|
|
172
|
+
### Query a Database
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createOrm, createTable, string, uuid } from "semola/orm";
|
|
176
|
+
|
|
177
|
+
const users = createTable("users", {
|
|
178
|
+
id: uuid("id").primaryKey(),
|
|
179
|
+
name: string("name").notNull(),
|
|
180
|
+
email: string("email").unique().notNull(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const db = createOrm({
|
|
184
|
+
url: "sqlite::memory:",
|
|
185
|
+
tables: { users },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Result-pattern query
|
|
189
|
+
const [findErr, rows] = await db.users.findMany({
|
|
190
|
+
where: { name: { contains: "John" } },
|
|
191
|
+
take: 10,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (findErr) {
|
|
195
|
+
console.error(findErr.type, findErr.message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Create record (result pattern)
|
|
199
|
+
const [createErr, user] = await db.users.create({
|
|
200
|
+
data: {
|
|
201
|
+
name: "John Doe",
|
|
202
|
+
email: "john@example.com",
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Low-level SQL-style methods are also available
|
|
207
|
+
const insertedRows = await db.users.insert({
|
|
208
|
+
data: {
|
|
209
|
+
name: "Jane Doe",
|
|
210
|
+
email: "jane@example.com",
|
|
211
|
+
},
|
|
212
|
+
returning: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log(rows, user, createErr, insertedRows);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Run ORM Migrations
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# create migration from schema diff
|
|
222
|
+
semola orm migrations create add-users
|
|
223
|
+
|
|
224
|
+
# apply pending migrations
|
|
225
|
+
semola orm migrations apply
|
|
226
|
+
|
|
227
|
+
# rollback last applied migration
|
|
228
|
+
semola orm migrations rollback
|
|
229
|
+
```
|
|
230
|
+
|
|
158
231
|
### Check Permissions
|
|
159
232
|
|
|
160
233
|
```typescript
|
|
@@ -272,12 +345,14 @@ _Higher is better for req/sec, lower is better for latency._
|
|
|
272
345
|
- [Queue](./docs/queue.md) - Redis-backed job queue with timeouts & concurrency
|
|
273
346
|
- [PubSub](./docs/pubsub.md) - Type-safe Redis pub/sub
|
|
274
347
|
- [Cron](./docs/cron.md) - In-memory cron scheduler for periodic task execution
|
|
348
|
+
- [Workflow](./docs/workflow.md) - Durable and resumable workflows with step persistence
|
|
275
349
|
- [Policy](./docs/policy.md) - Policy-based authorization
|
|
276
350
|
- [i18n](./docs/i18n.md) - Type-safe internationalization
|
|
277
351
|
- [Cache](./docs/cache.md) - Redis cache wrapper with TTL
|
|
278
352
|
- [Errors](./docs/errors.md) - Result-based error handling
|
|
279
353
|
- [Logging](./docs/logging.md) - Logging utility
|
|
280
354
|
- [Prompts](./docs/prompts.md) - Interactive CLI prompts
|
|
355
|
+
- [ORM](./docs/orm.md) - Type-safe data layer, result-pattern DX, and migrations
|
|
281
356
|
|
|
282
357
|
---
|
|
283
358
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/errors/types.d.ts
|
|
2
|
+
type CommonError = "NotFoundError" | "UnauthorizedError" | "InternalServerError" | "ValidationError" | "MigrationError" | "SchemaError" | (string & {});
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/lib/errors/index.d.ts
|
|
5
|
+
declare const ok: <T>(data: T) => readonly [null, T];
|
|
6
|
+
declare const err: <T extends CommonError>(type: T, message: string) => readonly [{
|
|
7
|
+
readonly type: T;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}, null];
|
|
10
|
+
declare const mightThrowSync: <T, E = Error>(fn: () => T) => readonly [null, T] | readonly [E, null];
|
|
11
|
+
declare const mightThrow: <T, E = Error>(promise: Promise<T>) => Promise<readonly [null, T] | readonly [E, null]>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ok as i, mightThrow as n, mightThrowSync as r, err as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/errors/types.d.ts
|
|
2
|
+
type CommonError = "NotFoundError" | "UnauthorizedError" | "InternalServerError" | "ValidationError" | "MigrationError" | "SchemaError" | (string & {});
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/lib/errors/index.d.ts
|
|
5
|
+
declare const ok: <T>(data: T) => readonly [null, T];
|
|
6
|
+
declare const err: <T extends CommonError>(type: T, message: string) => readonly [{
|
|
7
|
+
readonly type: T;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}, null];
|
|
10
|
+
declare const mightThrowSync: <T, E = Error>(fn: () => T) => readonly [null, T] | readonly [E, null];
|
|
11
|
+
declare const mightThrow: <T, E = Error>(promise: Promise<T>) => Promise<readonly [null, T] | readonly [E, null]>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ok as i, mightThrow as n, mightThrowSync as r, err as t };
|
package/dist/lib/api/index.cjs
CHANGED
|
@@ -1,5 +1,523 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
const require_lib_errors_index = require("../errors/index.cjs");
|
|
3
|
+
//#region src/lib/api/openapi/index.ts
|
|
4
|
+
const toOpenAPISchema = (schema, io = "input") => ({ schema: schema["~standard"].jsonSchema[io]({ target: "draft-2020-12" }) });
|
|
5
|
+
const getSchemaDescription = (schema) => {
|
|
6
|
+
const metadata = schema["~standard"];
|
|
7
|
+
if (!metadata) return "";
|
|
8
|
+
if ("description" in metadata && typeof metadata.description === "string") return metadata.description;
|
|
9
|
+
return "";
|
|
10
|
+
};
|
|
11
|
+
const requestFields = [
|
|
12
|
+
"body",
|
|
13
|
+
"query",
|
|
14
|
+
"headers",
|
|
15
|
+
"cookies",
|
|
16
|
+
"params"
|
|
17
|
+
];
|
|
18
|
+
const mergeRequestSchemas = (schemas) => {
|
|
19
|
+
const merged = {};
|
|
20
|
+
for (const schema of schemas) {
|
|
21
|
+
if (!schema) continue;
|
|
22
|
+
for (const field of requestFields) if (schema[field]) merged[field] = schema[field];
|
|
23
|
+
}
|
|
24
|
+
return merged;
|
|
25
|
+
};
|
|
26
|
+
const mergeResponseSchemas = (schemas) => {
|
|
27
|
+
const merged = {};
|
|
28
|
+
for (const schema of schemas) {
|
|
29
|
+
if (!schema) continue;
|
|
30
|
+
for (const status in schema) {
|
|
31
|
+
const statusCode = Number(status);
|
|
32
|
+
const responseSchema = schema[statusCode];
|
|
33
|
+
if (responseSchema) merged[statusCode] = responseSchema;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
37
|
+
};
|
|
38
|
+
const convertSchemaToOpenApi = async (schema, io = "input") => {
|
|
39
|
+
const result = toOpenAPISchema(schema, io);
|
|
40
|
+
const { schema: jsonSchema } = result;
|
|
41
|
+
const schemaId = jsonSchema.id;
|
|
42
|
+
if (schemaId && typeof schemaId === "string") {
|
|
43
|
+
const schemaWithoutId = { ...jsonSchema };
|
|
44
|
+
delete schemaWithoutId.id;
|
|
45
|
+
delete schemaWithoutId.$schema;
|
|
46
|
+
return {
|
|
47
|
+
schema: { $ref: `#/components/schemas/${schemaId}` },
|
|
48
|
+
components: { schemas: { [schemaId]: schemaWithoutId } }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (jsonSchema.$schema) {
|
|
52
|
+
const schemaWithoutMeta = { ...jsonSchema };
|
|
53
|
+
delete schemaWithoutMeta.$schema;
|
|
54
|
+
return {
|
|
55
|
+
schema: schemaWithoutMeta,
|
|
56
|
+
components: void 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
const convertSchemaToInlineOpenApi = async (schema, io = "input") => {
|
|
62
|
+
const { schema: jsonSchema } = toOpenAPISchema(schema, io);
|
|
63
|
+
const cleanSchema = { ...jsonSchema };
|
|
64
|
+
delete cleanSchema.$schema;
|
|
65
|
+
delete cleanSchema.id;
|
|
66
|
+
return {
|
|
67
|
+
schema: cleanSchema,
|
|
68
|
+
components: void 0
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const extractParametersFromSchema = async (schema, location) => {
|
|
72
|
+
const { schema: jsonSchema, components } = await convertSchemaToInlineOpenApi(schema);
|
|
73
|
+
if (jsonSchema.type !== "object") return {
|
|
74
|
+
parameters: [],
|
|
75
|
+
components
|
|
76
|
+
};
|
|
77
|
+
if (!jsonSchema.properties) return {
|
|
78
|
+
parameters: [],
|
|
79
|
+
components
|
|
80
|
+
};
|
|
81
|
+
const parameters = [];
|
|
82
|
+
const requiredFields = jsonSchema.required ?? [];
|
|
83
|
+
for (const name in jsonSchema.properties) {
|
|
84
|
+
const propertySchema = jsonSchema.properties[name];
|
|
85
|
+
const isRequired = requiredFields.includes(name);
|
|
86
|
+
parameters.push({
|
|
87
|
+
name,
|
|
88
|
+
in: location,
|
|
89
|
+
required: isRequired,
|
|
90
|
+
schema: propertySchema
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
parameters,
|
|
95
|
+
components
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const normalizePathForOpenAPI = (path) => {
|
|
99
|
+
return path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}");
|
|
100
|
+
};
|
|
101
|
+
const extractPathParameters = (path) => {
|
|
102
|
+
const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
103
|
+
if (!matches) return [];
|
|
104
|
+
return matches.map((match) => match.slice(1));
|
|
105
|
+
};
|
|
106
|
+
const paramSources = [
|
|
107
|
+
["query", "query"],
|
|
108
|
+
["headers", "header"],
|
|
109
|
+
["cookies", "cookie"]
|
|
110
|
+
];
|
|
111
|
+
const createParameters = async (request, path) => {
|
|
112
|
+
const parameters = [];
|
|
113
|
+
const allComponents = [];
|
|
114
|
+
for (const [field, location] of paramSources) if (request[field]) {
|
|
115
|
+
const { parameters: params, components } = await extractParametersFromSchema(request[field], location);
|
|
116
|
+
parameters.push(...params);
|
|
117
|
+
if (components) allComponents.push(components);
|
|
118
|
+
}
|
|
119
|
+
const pathParamNames = extractPathParameters(path);
|
|
120
|
+
if (pathParamNames.length > 0 && request.params) {
|
|
121
|
+
const { schema: jsonSchema, components } = await convertSchemaToInlineOpenApi(request.params);
|
|
122
|
+
if (components) allComponents.push(components);
|
|
123
|
+
if (jsonSchema.type === "object" && jsonSchema.properties) for (const name of pathParamNames) {
|
|
124
|
+
const propertySchema = jsonSchema.properties[name];
|
|
125
|
+
if (propertySchema) parameters.push({
|
|
126
|
+
name,
|
|
127
|
+
in: "path",
|
|
128
|
+
required: true,
|
|
129
|
+
schema: propertySchema
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
parameters,
|
|
135
|
+
components: allComponents
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const createRequestBody = async (bodySchema) => {
|
|
139
|
+
const { schema, components } = await convertSchemaToOpenApi(bodySchema);
|
|
140
|
+
return {
|
|
141
|
+
components,
|
|
142
|
+
requestBody: {
|
|
143
|
+
required: true,
|
|
144
|
+
content: { "application/json": { schema } }
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const createResponses = async (response) => {
|
|
149
|
+
const responses = {};
|
|
150
|
+
const allComponents = [];
|
|
151
|
+
if (!response) return {
|
|
152
|
+
responses,
|
|
153
|
+
components: allComponents
|
|
154
|
+
};
|
|
155
|
+
for (const status in response) {
|
|
156
|
+
const statusCode = String(status);
|
|
157
|
+
const schema = response[Number(status)];
|
|
158
|
+
if (!schema) continue;
|
|
159
|
+
const description = getSchemaDescription(schema);
|
|
160
|
+
const { schema: jsonSchema, components } = await convertSchemaToOpenApi(schema, "output");
|
|
161
|
+
if (components) allComponents.push(components);
|
|
162
|
+
responses[statusCode] = {
|
|
163
|
+
description: description || `Response with status ${statusCode}`,
|
|
164
|
+
content: { "application/json": { schema: jsonSchema } }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
responses,
|
|
169
|
+
components: allComponents
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const createOperation = async (route, globalMiddlewares, prefix) => {
|
|
173
|
+
const allMiddlewares = [...globalMiddlewares ?? [], ...route.middlewares ?? []];
|
|
174
|
+
const requestSchemas = [];
|
|
175
|
+
const responseSchemas = [];
|
|
176
|
+
for (const middleware of allMiddlewares) {
|
|
177
|
+
requestSchemas.push(middleware.options.request);
|
|
178
|
+
responseSchemas.push(middleware.options.response);
|
|
179
|
+
}
|
|
180
|
+
requestSchemas.push(route.request);
|
|
181
|
+
responseSchemas.push(route.response);
|
|
182
|
+
const mergedRequest = mergeRequestSchemas(requestSchemas);
|
|
183
|
+
const mergedResponse = mergeResponseSchemas(responseSchemas);
|
|
184
|
+
const { parameters, components: parameterComponents } = await createParameters(mergedRequest, prefix ? prefix + route.path : route.path);
|
|
185
|
+
const { responses, components: responseComponents } = await createResponses(mergedResponse);
|
|
186
|
+
const operation = { responses };
|
|
187
|
+
const allComponents = [];
|
|
188
|
+
allComponents.push(...responseComponents);
|
|
189
|
+
allComponents.push(...parameterComponents);
|
|
190
|
+
for (const field of [
|
|
191
|
+
"summary",
|
|
192
|
+
"description",
|
|
193
|
+
"operationId"
|
|
194
|
+
]) if (route[field]) operation[field] = route[field];
|
|
195
|
+
if (route.tags && route.tags.length > 0) operation.tags = route.tags;
|
|
196
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
197
|
+
const bodySchema = mergedRequest.body;
|
|
198
|
+
if (bodySchema) {
|
|
199
|
+
const { requestBody, components: bodyComponents } = await createRequestBody(bodySchema);
|
|
200
|
+
operation.requestBody = requestBody;
|
|
201
|
+
if (bodyComponents) allComponents.push(bodyComponents);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
operation,
|
|
205
|
+
components: allComponents
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const componentKeys = [
|
|
209
|
+
"schemas",
|
|
210
|
+
"responses",
|
|
211
|
+
"parameters",
|
|
212
|
+
"requestBodies"
|
|
213
|
+
];
|
|
214
|
+
const mergeComponents = (componentsArray) => {
|
|
215
|
+
const merged = {};
|
|
216
|
+
for (const components of componentsArray) {
|
|
217
|
+
if (!components) continue;
|
|
218
|
+
for (const key of componentKeys) if (components[key]) merged[key] = {
|
|
219
|
+
...merged[key],
|
|
220
|
+
...components[key]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return merged;
|
|
224
|
+
};
|
|
225
|
+
const generateOpenApiSpec = async (options) => {
|
|
226
|
+
const spec = {
|
|
227
|
+
openapi: "3.1.0",
|
|
228
|
+
info: {
|
|
229
|
+
title: options.title,
|
|
230
|
+
description: options.description,
|
|
231
|
+
version: options.version
|
|
232
|
+
},
|
|
233
|
+
paths: {}
|
|
234
|
+
};
|
|
235
|
+
if (options.servers && options.servers.length > 0) spec.servers = options.servers;
|
|
236
|
+
if (options.securitySchemes) spec.components = { securitySchemes: options.securitySchemes };
|
|
237
|
+
const allRouteComponents = [];
|
|
238
|
+
for (const route of options.routes) {
|
|
239
|
+
const openApiPath = normalizePathForOpenAPI(options.prefix ? options.prefix + route.path : route.path);
|
|
240
|
+
const method = route.method.toLowerCase();
|
|
241
|
+
if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
|
|
242
|
+
const { operation, components } = await createOperation(route, options.globalMiddlewares ?? [], options.prefix);
|
|
243
|
+
spec.paths[openApiPath][method] = operation;
|
|
244
|
+
allRouteComponents.push(...components);
|
|
245
|
+
}
|
|
246
|
+
const mergedComponents = mergeComponents(allRouteComponents);
|
|
247
|
+
if (!spec.components) spec.components = {};
|
|
248
|
+
if (options.securitySchemes) spec.components.securitySchemes = options.securitySchemes;
|
|
249
|
+
for (const key of componentKeys) {
|
|
250
|
+
const value = mergedComponents[key];
|
|
251
|
+
if (value && Object.keys(value).length > 0) spec.components[key] = value;
|
|
252
|
+
}
|
|
253
|
+
return spec;
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/lib/api/validation/index.ts
|
|
257
|
+
const validateSchema = async (schema, data) => {
|
|
258
|
+
const result = await schema["~standard"].validate(data);
|
|
259
|
+
if (!result.issues) return require_lib_errors_index.ok(result.value);
|
|
260
|
+
return require_lib_errors_index.err("ValidationError", result.issues.map((issue) => {
|
|
261
|
+
let path = "unknown";
|
|
262
|
+
if (Array.isArray(issue.path)) path = issue.path.map(String).join(".");
|
|
263
|
+
return `${path}: ${issue.message ?? "validation failed"}`;
|
|
264
|
+
}).join(", "));
|
|
265
|
+
};
|
|
266
|
+
const validateBody = async (req, bodySchema, bodyCache) => {
|
|
267
|
+
if (!bodySchema) return require_lib_errors_index.ok(true);
|
|
268
|
+
if (!(req.headers.get("content-type") ?? "").includes("application/json")) return require_lib_errors_index.ok(void 0);
|
|
269
|
+
if (bodyCache?.parsed) return validateSchema(bodySchema, bodyCache.value);
|
|
270
|
+
const [parseError, parsedBody] = await require_lib_errors_index.mightThrow(req.json());
|
|
271
|
+
if (parseError) return require_lib_errors_index.err("ParseError", "Invalid JSON body");
|
|
272
|
+
if (bodyCache) {
|
|
273
|
+
bodyCache.parsed = true;
|
|
274
|
+
bodyCache.value = parsedBody;
|
|
275
|
+
}
|
|
276
|
+
return validateSchema(bodySchema, parsedBody);
|
|
277
|
+
};
|
|
278
|
+
const validateQuery = async (req, querySchema) => {
|
|
279
|
+
if (!querySchema) return require_lib_errors_index.ok(true);
|
|
280
|
+
const qIndex = req.url.indexOf("?");
|
|
281
|
+
if (qIndex === -1) return validateSchema(querySchema, {});
|
|
282
|
+
const hashIndex = req.url.indexOf("#", qIndex + 1);
|
|
283
|
+
const queryString = hashIndex === -1 ? req.url.slice(qIndex + 1) : req.url.slice(qIndex + 1, hashIndex);
|
|
284
|
+
const searchParams = new URLSearchParams(queryString);
|
|
285
|
+
const queryParams = {};
|
|
286
|
+
for (const key of searchParams.keys()) {
|
|
287
|
+
const values = searchParams.getAll(key);
|
|
288
|
+
const [firstValue] = values;
|
|
289
|
+
if (values.length === 1) queryParams[key] = firstValue;
|
|
290
|
+
else queryParams[key] = values;
|
|
291
|
+
}
|
|
292
|
+
return validateSchema(querySchema, queryParams);
|
|
293
|
+
};
|
|
294
|
+
const validateHeaders = async (req, headersSchema) => {
|
|
295
|
+
if (!headersSchema) return require_lib_errors_index.ok(true);
|
|
296
|
+
const headers = {};
|
|
297
|
+
req.headers.forEach((value, key) => {
|
|
298
|
+
headers[key] = value;
|
|
299
|
+
});
|
|
300
|
+
return validateSchema(headersSchema, headers);
|
|
301
|
+
};
|
|
302
|
+
const validateCookies = async (req, cookiesSchema) => {
|
|
303
|
+
if (!cookiesSchema) return require_lib_errors_index.ok(true);
|
|
304
|
+
const cookieHeader = req.headers.get("cookie") ?? "";
|
|
305
|
+
const cookieMap = new Bun.CookieMap(cookieHeader);
|
|
306
|
+
return validateSchema(cookiesSchema, Object.fromEntries(cookieMap));
|
|
307
|
+
};
|
|
308
|
+
const validateParams = async (req, paramsSchema) => {
|
|
309
|
+
if (!paramsSchema) return require_lib_errors_index.ok(true);
|
|
310
|
+
return validateSchema(paramsSchema, req.params);
|
|
311
|
+
};
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/lib/api/core/index.ts
|
|
314
|
+
const defaultValidated = Object.freeze({
|
|
315
|
+
body: void 0,
|
|
316
|
+
query: void 0,
|
|
317
|
+
headers: void 0,
|
|
318
|
+
cookies: void 0,
|
|
319
|
+
params: void 0
|
|
320
|
+
});
|
|
321
|
+
const responseHelpers = {
|
|
322
|
+
json: (status, data) => Response.json(data, { status }),
|
|
323
|
+
text: (status, text) => new Response(text, { status }),
|
|
324
|
+
html: (status, html) => new Response(html, {
|
|
325
|
+
status,
|
|
326
|
+
headers: { "Content-Type": "text/html" }
|
|
327
|
+
}),
|
|
328
|
+
redirect: (status, url) => Response.redirect(url, status)
|
|
329
|
+
};
|
|
330
|
+
const noopGet = () => void 0;
|
|
331
|
+
const stripTrailingSlash = (path) => {
|
|
332
|
+
if (path !== "/" && path.endsWith("/")) return path.slice(0, -1);
|
|
333
|
+
return path;
|
|
334
|
+
};
|
|
335
|
+
const hasSchemas = (schema) => schema && (schema.body || schema.query || schema.headers || schema.cookies || schema.params);
|
|
336
|
+
const needsBodyCache = (schema) => schema?.body !== void 0;
|
|
337
|
+
const shouldCreateBodyCache = (hasMiddlewares, allMiddlewares, request) => {
|
|
338
|
+
if (needsBodyCache(request)) return true;
|
|
339
|
+
if (!hasMiddlewares) return false;
|
|
340
|
+
return allMiddlewares.some((mw) => needsBodyCache(mw.options.request));
|
|
341
|
+
};
|
|
342
|
+
const resolveValidation = (v) => {
|
|
343
|
+
if (v === void 0 || v === true) return {
|
|
344
|
+
input: true,
|
|
345
|
+
output: true
|
|
346
|
+
};
|
|
347
|
+
if (v === false) return {
|
|
348
|
+
input: false,
|
|
349
|
+
output: false
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
input: v.input !== false,
|
|
353
|
+
output: v.output !== false
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
var Api = class {
|
|
357
|
+
options;
|
|
358
|
+
routes = [];
|
|
359
|
+
constructor(options = {}) {
|
|
360
|
+
this.options = options;
|
|
361
|
+
}
|
|
362
|
+
getFullPath(path) {
|
|
363
|
+
const normalizedPath = stripTrailingSlash(path) || "/";
|
|
364
|
+
if (!this.options.prefix) return normalizedPath;
|
|
365
|
+
const normalizedPrefix = stripTrailingSlash(this.options.prefix);
|
|
366
|
+
if (normalizedPrefix === "/") return normalizedPath;
|
|
367
|
+
if (normalizedPath === "/") return normalizedPrefix;
|
|
368
|
+
return normalizedPrefix + normalizedPath;
|
|
369
|
+
}
|
|
370
|
+
async validateRequestSchema(req, schema, bodyCache) {
|
|
371
|
+
if (!schema) return {
|
|
372
|
+
success: true,
|
|
373
|
+
data: {}
|
|
374
|
+
};
|
|
375
|
+
const v = {};
|
|
376
|
+
if (schema.body) {
|
|
377
|
+
const [err, val] = await validateBody(req, schema.body, bodyCache);
|
|
378
|
+
if (err) return {
|
|
379
|
+
success: false,
|
|
380
|
+
error: err
|
|
381
|
+
};
|
|
382
|
+
v.body = val;
|
|
383
|
+
}
|
|
384
|
+
if (schema.query) {
|
|
385
|
+
const [err, val] = await validateQuery(req, schema.query);
|
|
386
|
+
if (err) return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: err
|
|
389
|
+
};
|
|
390
|
+
v.query = val;
|
|
391
|
+
}
|
|
392
|
+
if (schema.headers) {
|
|
393
|
+
const [err, val] = await validateHeaders(req, schema.headers);
|
|
394
|
+
if (err) return {
|
|
395
|
+
success: false,
|
|
396
|
+
error: err
|
|
397
|
+
};
|
|
398
|
+
v.headers = val;
|
|
399
|
+
}
|
|
400
|
+
if (schema.cookies) {
|
|
401
|
+
const [err, val] = await validateCookies(req, schema.cookies);
|
|
402
|
+
if (err) return {
|
|
403
|
+
success: false,
|
|
404
|
+
error: err
|
|
405
|
+
};
|
|
406
|
+
v.cookies = val;
|
|
407
|
+
}
|
|
408
|
+
if (schema.params) {
|
|
409
|
+
const [err, val] = await validateParams(req, schema.params);
|
|
410
|
+
if (err) return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: err
|
|
413
|
+
};
|
|
414
|
+
v.params = val;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
success: true,
|
|
418
|
+
data: v
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
createContext(req, validated, extensions) {
|
|
422
|
+
return {
|
|
423
|
+
raw: req,
|
|
424
|
+
req: validated,
|
|
425
|
+
...responseHelpers,
|
|
426
|
+
get: (key) => extensions[key]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async validateResponseBody(response, schema) {
|
|
430
|
+
if (!schema) return response;
|
|
431
|
+
const statusSchema = schema[response.status];
|
|
432
|
+
if (!statusSchema) return response;
|
|
433
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
|
|
434
|
+
const [parseError, body] = await require_lib_errors_index.mightThrow(response.clone().json());
|
|
435
|
+
if (parseError) return responseHelpers.json(400, { message: "Invalid response body" });
|
|
436
|
+
const [validationError] = await validateSchema(statusSchema, body);
|
|
437
|
+
if (validationError) return responseHelpers.json(400, { message: validationError.message });
|
|
438
|
+
return response;
|
|
439
|
+
}
|
|
440
|
+
buildBunRoutes() {
|
|
441
|
+
const bunRoutes = {};
|
|
442
|
+
const validationConfig = resolveValidation(this.options.validation);
|
|
443
|
+
for (const route of this.routes) {
|
|
444
|
+
const { path, method, handler, request, response, middlewares } = route;
|
|
445
|
+
const fullPath = this.getFullPath(path);
|
|
446
|
+
if (!bunRoutes[fullPath]) bunRoutes[fullPath] = {};
|
|
447
|
+
const allMiddlewares = [...this.options.middlewares ?? [], ...middlewares ?? []];
|
|
448
|
+
const hasMiddlewares = allMiddlewares.length > 0;
|
|
449
|
+
const hasRouteSchemas = hasSchemas(request);
|
|
450
|
+
const effectiveOutputValidation = validationConfig.output && !!response;
|
|
451
|
+
if (!hasMiddlewares && !(validationConfig.input && hasRouteSchemas) && !effectiveOutputValidation) bunRoutes[fullPath][method] = (req) => {
|
|
452
|
+
const context = Object.create(responseHelpers);
|
|
453
|
+
context.raw = req;
|
|
454
|
+
context.req = defaultValidated;
|
|
455
|
+
context.get = noopGet;
|
|
456
|
+
return handler(context);
|
|
457
|
+
};
|
|
458
|
+
else bunRoutes[fullPath][method] = async (req) => {
|
|
459
|
+
const extensions = {};
|
|
460
|
+
const bodyCache = validationConfig.input && shouldCreateBodyCache(hasMiddlewares, allMiddlewares, request) ? {
|
|
461
|
+
parsed: false,
|
|
462
|
+
value: void 0
|
|
463
|
+
} : void 0;
|
|
464
|
+
for (const mw of allMiddlewares) {
|
|
465
|
+
const { request: reqSchema, handler: mwHandler } = mw.options;
|
|
466
|
+
let validated = defaultValidated;
|
|
467
|
+
if (validationConfig.input && hasSchemas(reqSchema)) {
|
|
468
|
+
const result = await this.validateRequestSchema(req, reqSchema, bodyCache);
|
|
469
|
+
if (!result.success) return responseHelpers.json(400, { message: result.error?.message });
|
|
470
|
+
validated = result.data;
|
|
471
|
+
}
|
|
472
|
+
const mwResult = await mwHandler(this.createContext(req, validated, extensions));
|
|
473
|
+
if (mwResult instanceof Response) return mwResult;
|
|
474
|
+
if (mwResult) Object.assign(extensions, mwResult);
|
|
475
|
+
}
|
|
476
|
+
let routeValidated = defaultValidated;
|
|
477
|
+
if (validationConfig.input && hasRouteSchemas) {
|
|
478
|
+
const result = await this.validateRequestSchema(req, request, bodyCache);
|
|
479
|
+
if (!result.success) return responseHelpers.json(400, { message: result.error?.message });
|
|
480
|
+
routeValidated = result.data;
|
|
481
|
+
}
|
|
482
|
+
const handlerResponse = await handler(this.createContext(req, routeValidated, extensions));
|
|
483
|
+
if (effectiveOutputValidation) return this.validateResponseBody(handlerResponse, response);
|
|
484
|
+
return handlerResponse;
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return bunRoutes;
|
|
488
|
+
}
|
|
489
|
+
defineRoute(config) {
|
|
490
|
+
this.routes.push(config);
|
|
491
|
+
}
|
|
492
|
+
getOpenApiSpec() {
|
|
493
|
+
return generateOpenApiSpec({
|
|
494
|
+
title: this.options.openapi?.title ?? "API",
|
|
495
|
+
description: this.options.openapi?.description,
|
|
496
|
+
version: this.options.openapi?.version ?? "1.0.0",
|
|
497
|
+
prefix: this.options.prefix,
|
|
498
|
+
servers: this.options.openapi?.servers,
|
|
499
|
+
securitySchemes: this.options.openapi?.securitySchemes,
|
|
500
|
+
routes: this.routes,
|
|
501
|
+
globalMiddlewares: this.options.middlewares
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
serve(port, callback) {
|
|
505
|
+
const bunRoutes = this.buildBunRoutes();
|
|
506
|
+
const server = Bun.serve({
|
|
507
|
+
port,
|
|
508
|
+
routes: bunRoutes,
|
|
509
|
+
fetch: () => new Response("Not found", { status: 404 })
|
|
510
|
+
});
|
|
511
|
+
if (callback) callback(server);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/lib/api/middleware/index.ts
|
|
516
|
+
var Middleware = class {
|
|
517
|
+
constructor(options) {
|
|
518
|
+
this.options = options;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
//#endregion
|
|
522
|
+
exports.Api = Api;
|
|
523
|
+
exports.Middleware = Middleware;
|