futonic 0.1.0 → 0.1.1-canary.e9561d3
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 +219 -0
- package/dist/{chunk-j2yky04y.js → chunk-eqeddyek.js} +5 -5
- package/dist/{chunk-6w1vqkpr.js → chunk-s3j6ndfy.js} +11 -2
- package/dist/{chunk-q65vm76v.js → chunk-s9sdjw1m.js} +3 -2
- package/dist/chunk-v0bahtg2.js +4 -0
- package/dist/cli/generators/kysely.d.ts.map +1 -1
- package/dist/cli/generators/prisma.d.ts.map +1 -1
- package/dist/cli/index.js +6 -7
- package/dist/client/index.js +1 -1
- package/dist/core/host.d.ts +1 -1
- package/dist/core/host.d.ts.map +1 -1
- package/dist/db/internal-adapter.d.ts +21 -1
- package/dist/db/internal-adapter.d.ts.map +1 -1
- package/dist/db/kysely-factory.d.ts.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -79
- package/dist/router/endpoint.d.ts.map +1 -1
- package/package.json +68 -62
- package/dist/chunk-5va59f7m.js +0 -20
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Futonic 🛋️
|
|
2
|
+
|
|
3
|
+
**A framework for building services that embed into host applications instead of deploying alongside them.**
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
There are plenty of services available as easy-to-deploy containers — auth servers, payment processors, observability tools. But for most applications, deploying multiple containers (one for the main app plus one for each service) is a waste. An auth server, a payment webhook handler, and an observability stack don't each need their own process. They could share compute and a database without any meaningful noisy-neighbor issues — because let's be honest, most apps these days are just wrappers around heavier services anyway.
|
|
8
|
+
|
|
9
|
+
## The idea
|
|
10
|
+
|
|
11
|
+
Futonic is heavily inspired by [better-auth](https://github.com/better-auth/better-auth)'s service embedding paradigm — the insight that many services don't need their own process, their own deployment, or even their own database. They just need a place to crash.
|
|
12
|
+
|
|
13
|
+
Futonic is a framework for building **embeddable services** that crash on a host application's futon. They share the host's compute. They share the host's database. They wake up when needed and stay out of the way when they're not.
|
|
14
|
+
|
|
15
|
+
No separate containers. No extra Dockerfiles. No internal networking. Just services that live inside the host app — and a great DX for building them.
|
|
16
|
+
|
|
17
|
+
## Why build embeddable services
|
|
18
|
+
|
|
19
|
+
**Your users save money:**
|
|
20
|
+
Most apps aren't mega-scale. They're tools, SaaS products, and indie projects where every dollar of infrastructure counts. When your service embeds directly into the host, your users don't need to pay for another container sitting idle 99% of the time.
|
|
21
|
+
|
|
22
|
+
**Your users get a better dev experience:**
|
|
23
|
+
No more `docker-compose up` with 6 services just to work on a feature. No more debugging why the auth container can't talk to the payments container on a laptop. Embedded services run in-process. Switch branches, switch worktrees — everything just works.
|
|
24
|
+
|
|
25
|
+
**Simplicity sells:**
|
|
26
|
+
Fewer moving parts means fewer things that break at 2am. One deploy. One database. One set of logs. Developers can always decompose later if they outgrow it — but most apps never will. The easier your service is to adopt, the more people will use it.
|
|
27
|
+
|
|
28
|
+
## How it works: the vertical slice
|
|
29
|
+
|
|
30
|
+
A futonic service is a **vertical slice** of your application. It defines everything from the client interface down to the database — API endpoints, input validation, business logic, and table schemas — as a single, self-contained unit.
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
block-beta
|
|
34
|
+
columns 3
|
|
35
|
+
block:host["Host Application"]:3
|
|
36
|
+
columns 3
|
|
37
|
+
space:3
|
|
38
|
+
block:app["Your App Code"]:1
|
|
39
|
+
columns 1
|
|
40
|
+
appRoutes["Routes"]
|
|
41
|
+
appLogic["Logic"]
|
|
42
|
+
appTables["Tables"]
|
|
43
|
+
end
|
|
44
|
+
block:billing["@acme/billing"]:1
|
|
45
|
+
columns 1
|
|
46
|
+
billingAPI["Endpoints"]
|
|
47
|
+
billingLogic["Logic"]
|
|
48
|
+
billingTables["Tables"]
|
|
49
|
+
end
|
|
50
|
+
block:auth["@acme/auth"]:1
|
|
51
|
+
columns 1
|
|
52
|
+
authAPI["Endpoints"]
|
|
53
|
+
authLogic["Logic"]
|
|
54
|
+
authTables["Tables"]
|
|
55
|
+
end
|
|
56
|
+
space:3
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
style billing stroke:#22c55e,stroke-width:2px
|
|
60
|
+
style auth stroke:#22c55e,stroke-width:2px
|
|
61
|
+
style app stroke:#6366f1,stroke-width:2px
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each green block is a futonic service — a vertical slice that owns its entire stack, published as an npm package. The host application mounts them alongside its own code. They all share one process, one database, one deployment.
|
|
65
|
+
|
|
66
|
+
This is what makes futonic different from microservices. A microservice is also a vertical slice, but it pays for that independence with its own container, its own deployment pipeline, and network hops between every call. A futonic service gets the same self-containment for free:
|
|
67
|
+
|
|
68
|
+
- **No deployment cost.** It runs inside your existing process. No extra containers, no extra infrastructure, no extra cloud bill.
|
|
69
|
+
- **No networking latency.** Service calls from backend code are direct function calls — same process, same database connection pool, zero serialization.
|
|
70
|
+
- **No protocol constraints.** Endpoints are built on web standard `Request`/`Response`, so a service can return JSON, full HTML pages, server-sent event streams, file downloads — anything `Response` supports.
|
|
71
|
+
|
|
72
|
+
### What a service looks like
|
|
73
|
+
|
|
74
|
+
A service defines its database tables and API endpoints. The service author publishes this as an npm package:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// @acme/billing
|
|
78
|
+
|
|
79
|
+
import { createService, createEndpoint } from "futonic";
|
|
80
|
+
|
|
81
|
+
export const billing = createService({
|
|
82
|
+
id: "billing",
|
|
83
|
+
version: "0.1.0",
|
|
84
|
+
dependencies: { database: true },
|
|
85
|
+
dbSchema: {
|
|
86
|
+
tables: {
|
|
87
|
+
invoices: {
|
|
88
|
+
fields: {
|
|
89
|
+
id: { type: "string", primaryKey: true, required: true },
|
|
90
|
+
amount: { type: "number", required: true },
|
|
91
|
+
status: { type: "string", required: true },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Endpoints receive a `ServiceContext` via middleware — giving them scoped access to the service's database tables, config, and logger:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
export function createBillingEndpoints(use: Middleware[]) {
|
|
103
|
+
return {
|
|
104
|
+
createInvoice: createEndpoint(
|
|
105
|
+
"/invoices",
|
|
106
|
+
{ method: "POST", use, body: z.object({ amount: z.number(), status: z.string() }) },
|
|
107
|
+
async (ctx) => {
|
|
108
|
+
const svc = ctx.context.serviceCtx;
|
|
109
|
+
const invoice = await svc.db.invoices.create({
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
...ctx.body,
|
|
112
|
+
});
|
|
113
|
+
svc.logger.info(`Invoice created: ${invoice.id}`);
|
|
114
|
+
return invoice;
|
|
115
|
+
},
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The endpoint writes to `svc.db.invoices`, but the actual table in the database is `billing_invoices` — automatically prefixed with the service ID. A second service with its own `invoices` table would hit a completely separate table. Same database, no collisions.
|
|
122
|
+
|
|
123
|
+
### How the host mounts it
|
|
124
|
+
|
|
125
|
+
The host developer installs the service and wires it in. Futonic auto-detects the database driver — `pg`, `mysql2`, `better-sqlite3`, or `bun:sqlite`:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { createHost } from "futonic";
|
|
129
|
+
import { billing, createBillingRouter } from "@acme/billing";
|
|
130
|
+
|
|
131
|
+
const host = createHost({
|
|
132
|
+
database: pool, // Their existing pg.Pool, mysql2 pool, or sqlite instance
|
|
133
|
+
baseURL: "http://localhost:3000",
|
|
134
|
+
services: [
|
|
135
|
+
billing({ mount: "/api/billing" }),
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await host.init();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then a single catch-all route in their framework of choice:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Hono
|
|
146
|
+
app.all("/api/billing/*", (c) => billingRouter.handler(c.req.raw));
|
|
147
|
+
|
|
148
|
+
// Next.js — app/api/billing/[...path]/route.ts
|
|
149
|
+
import { toNextJsHandler } from "futonic/next";
|
|
150
|
+
export const { GET, POST, PUT, DELETE, PATCH } = toNextJsHandler(billingRouter);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That's it. The billing service handles requests at `/api/billing/*`, stores data in the host's database under prefixed tables, and the host didn't need to understand any of the service's internals to set it up.
|
|
154
|
+
|
|
155
|
+
## More features
|
|
156
|
+
|
|
157
|
+
### Type-safe RPC from the frontend
|
|
158
|
+
|
|
159
|
+
Host developers consume your service from their frontend with a fully typed client — autocomplete, return types, and error types included:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { createClient } from "futonic/client";
|
|
163
|
+
import type { BillingRouter } from "@acme/billing";
|
|
164
|
+
|
|
165
|
+
const billing = createClient<BillingRouter>({
|
|
166
|
+
baseURL: "/api/billing",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const { data } = await billing.listInvoices();
|
|
170
|
+
const { data: invoice } = await billing.createInvoice({
|
|
171
|
+
body: { amount: 4200, status: "draft" },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Skip networking on the backend
|
|
176
|
+
|
|
177
|
+
When the host's backend code needs to call the service, there's no HTTP round-trip. The service runs in the same process, shares the same database connection pool, and returns results directly:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const svc = host.services.get("billing").serviceContext!;
|
|
181
|
+
const invoices = await svc.db.invoices.findMany();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Generate migrations for any ORM
|
|
185
|
+
|
|
186
|
+
Ship a CLI so your users can generate migration files for their ORM of choice:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npx @acme/billing generate --orm=drizzle --provider=pg --out=schema.ts
|
|
190
|
+
npx @acme/billing generate --orm=prisma --provider=mysql
|
|
191
|
+
npx @acme/billing generate --orm=kysely --provider=sqlite
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Return any web standard response
|
|
195
|
+
|
|
196
|
+
Endpoints aren't limited to JSON. Serve full HTML pages for embedded UIs, server-sent event streams for real-time updates, file downloads, redirects — whatever `Response` supports. Build an entire admin dashboard that lives inside the host app.
|
|
197
|
+
|
|
198
|
+
## Things you could build
|
|
199
|
+
|
|
200
|
+
- **Auth service** — User management, sessions, OAuth flows, and permissions. Like better-auth, but packaged as an embeddable service.
|
|
201
|
+
- **Payment service** — Webhook handlers, invoice management, subscription state. A reusable Stripe integration that anyone can drop into their app.
|
|
202
|
+
- **Observability service** — An API for ingesting traces, database tables for storing them, and an embedded UI for visualizing them. Jaeger without the deployment.
|
|
203
|
+
- **Feature flags** — A flag service with an admin UI, backed by the host's existing database.
|
|
204
|
+
- **CMS** — Content management endpoints with an embedded editor interface.
|
|
205
|
+
- **Notifications** — Email/push queue management with status tracking and retry logic.
|
|
206
|
+
|
|
207
|
+
## Database support
|
|
208
|
+
|
|
209
|
+
Futonic auto-detects your database driver and works with:
|
|
210
|
+
|
|
211
|
+
- **PostgreSQL** via `pg`
|
|
212
|
+
- **MySQL** via `mysql2`
|
|
213
|
+
- **SQLite** via `better-sqlite3` or Bun's built-in `bun:sqlite`
|
|
214
|
+
|
|
215
|
+
Your service works with whatever database the host is already running — no extra configuration on your end.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
__require
|
|
3
|
-
|
|
4
|
-
} from "./chunk-5va59f7m.js";
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-v0bahtg2.js";
|
|
5
4
|
|
|
6
5
|
// src/cli/generators/prisma.ts
|
|
7
6
|
import { existsSync } from "node:fs";
|
|
@@ -63,14 +62,15 @@ var generatePrismaSchema = async ({
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
if (field.references) {
|
|
66
|
-
const
|
|
65
|
+
const referencedTable = `${table.serviceId}_${field.references.model}`;
|
|
66
|
+
const refModelName = toPascalCase(referencedTable);
|
|
67
67
|
let action = "Cascade";
|
|
68
68
|
if (field.references.onDelete === "restrict")
|
|
69
69
|
action = "Restrict";
|
|
70
70
|
else if (field.references.onDelete === "set-null")
|
|
71
71
|
action = "SetNull";
|
|
72
72
|
const relationAttr = `relation(fields: [${fieldName}], references: [${field.references.field}], onDelete: ${action})`;
|
|
73
|
-
builder.model(modelName).field(
|
|
73
|
+
builder.model(modelName).field(referencedTable.toLowerCase(), `${refModelName}${field.required ? "" : "?"}`).attribute(relationAttr);
|
|
74
74
|
}
|
|
75
75
|
if (provider === "mysql" && field.type === "string" && !field.unique && !field.references) {
|
|
76
76
|
fieldBuilder.attribute("db.Text");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"./chunk-
|
|
1
|
+
import"./chunk-v0bahtg2.js";
|
|
2
2
|
|
|
3
3
|
// src/cli/generators/kysely.ts
|
|
4
4
|
var generateKyselySchema = async ({
|
|
@@ -27,7 +27,8 @@ function tableToSQL(table, provider = "pg") {
|
|
|
27
27
|
if (field.references) {
|
|
28
28
|
const onDelete = field.references.onDelete ?? "restrict";
|
|
29
29
|
const action = onDelete === "set-null" ? "SET NULL" : onDelete.toUpperCase();
|
|
30
|
-
|
|
30
|
+
const referencedTable = `${table.serviceId}_${field.references.model}`;
|
|
31
|
+
columns.push(` FOREIGN KEY (${fieldName}) REFERENCES ${referencedTable}(${field.references.field}) ON DELETE ${action}`);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
return `CREATE TABLE IF NOT EXISTS ${table.prefixedName} (
|
|
@@ -62,6 +63,14 @@ function fieldToSQLType(field, provider) {
|
|
|
62
63
|
else
|
|
63
64
|
type = "TEXT";
|
|
64
65
|
break;
|
|
66
|
+
case "binary":
|
|
67
|
+
if (provider === "pg")
|
|
68
|
+
type = "BYTEA";
|
|
69
|
+
else if (provider === "mysql")
|
|
70
|
+
type = "LONGBLOB";
|
|
71
|
+
else
|
|
72
|
+
type = "BLOB";
|
|
73
|
+
break;
|
|
65
74
|
default:
|
|
66
75
|
type = "TEXT";
|
|
67
76
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"./chunk-
|
|
1
|
+
import"./chunk-v0bahtg2.js";
|
|
2
2
|
|
|
3
3
|
// src/cli/generators/drizzle.ts
|
|
4
4
|
var generateDrizzleSchema = async ({
|
|
@@ -48,7 +48,8 @@ function generateTable(table, provider) {
|
|
|
48
48
|
if (field.references) {
|
|
49
49
|
const onDelete = field.references.onDelete || "cascade";
|
|
50
50
|
const action = onDelete === "set-null" ? "set null" : onDelete;
|
|
51
|
-
|
|
51
|
+
const referencedTable = `${table.serviceId}_${field.references.model}`;
|
|
52
|
+
col += `.references(() => ${referencedTable}.${field.references.field}, { onDelete: '${action}' })`;
|
|
52
53
|
}
|
|
53
54
|
return ` ${fieldName}: ${col}`;
|
|
54
55
|
}).join(`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/kysely.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEjE,eAAO,MAAM,oBAAoB,EAAE,eAmBlC,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,aAAa,EACpB,QAAQ,GAAE,gBAAuB,GAC/B,MAAM,
|
|
1
|
+
{"version":3,"file":"kysely.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/kysely.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEjE,eAAO,MAAM,oBAAoB,EAAE,eAmBlC,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,aAAa,EACpB,QAAQ,GAAE,gBAAuB,GAC/B,MAAM,CAqBR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/prisma.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,eAAO,MAAM,oBAAoB,EAAE,
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../../src/cli/generators/prisma.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,eAAO,MAAM,oBAAoB,EAAE,eAgIlC,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -2,12 +2,11 @@ import {
|
|
|
2
2
|
getServiceTables
|
|
3
3
|
} from "../chunk-nj3a3jzm.js";
|
|
4
4
|
import {
|
|
5
|
-
__require
|
|
6
|
-
|
|
7
|
-
} from "../chunk-5va59f7m.js";
|
|
5
|
+
__require
|
|
6
|
+
} from "../chunk-v0bahtg2.js";
|
|
8
7
|
|
|
9
8
|
// src/cli/index.ts
|
|
10
|
-
import {
|
|
9
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
function createCLI(options) {
|
|
13
12
|
const args = process.argv.slice(2);
|
|
@@ -80,15 +79,15 @@ async function runGenerate(flags, options) {
|
|
|
80
79
|
async function loadGenerator(orm) {
|
|
81
80
|
switch (orm) {
|
|
82
81
|
case "drizzle": {
|
|
83
|
-
const { generateDrizzleSchema } = await import("../chunk-
|
|
82
|
+
const { generateDrizzleSchema } = await import("../chunk-s9sdjw1m.js");
|
|
84
83
|
return generateDrizzleSchema;
|
|
85
84
|
}
|
|
86
85
|
case "prisma": {
|
|
87
|
-
const { generatePrismaSchema } = await import("../chunk-
|
|
86
|
+
const { generatePrismaSchema } = await import("../chunk-eqeddyek.js");
|
|
88
87
|
return generatePrismaSchema;
|
|
89
88
|
}
|
|
90
89
|
case "kysely": {
|
|
91
|
-
const { generateKyselySchema } = await import("../chunk-
|
|
90
|
+
const { generateKyselySchema } = await import("../chunk-s3j6ndfy.js");
|
|
92
91
|
return generateKyselySchema;
|
|
93
92
|
}
|
|
94
93
|
default:
|
package/dist/client/index.js
CHANGED
package/dist/core/host.d.ts
CHANGED
package/dist/core/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,kBAAkB,EAEvB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,IAAI;IACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACtC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAgFnD"}
|
|
@@ -14,8 +14,25 @@ export interface Where {
|
|
|
14
14
|
operator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "not_in";
|
|
15
15
|
connector?: "AND" | "OR";
|
|
16
16
|
}
|
|
17
|
+
export type FilterOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "not_in" | "contains" | "startsWith" | "endsWith" | "isNull" | "isNotNull";
|
|
18
|
+
export type FilterNode = {
|
|
19
|
+
type: "and" | "or";
|
|
20
|
+
nodes: FilterNode[];
|
|
21
|
+
} | {
|
|
22
|
+
type: "not";
|
|
23
|
+
node: FilterNode;
|
|
24
|
+
} | {
|
|
25
|
+
type: "cond";
|
|
26
|
+
field: string;
|
|
27
|
+
op: FilterOp;
|
|
28
|
+
value?: unknown;
|
|
29
|
+
};
|
|
17
30
|
export interface FindManyOptions {
|
|
18
31
|
where?: Where[];
|
|
32
|
+
/** Boolean expression tree (supports and/or/not + contains/like); combined with `where`. */
|
|
33
|
+
filter?: FilterNode;
|
|
34
|
+
/** Column projection; when omitted selects all columns. */
|
|
35
|
+
select?: string[];
|
|
19
36
|
limit?: number;
|
|
20
37
|
offset?: number;
|
|
21
38
|
sortBy?: {
|
|
@@ -27,7 +44,10 @@ export interface TableAdapter {
|
|
|
27
44
|
create(data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
28
45
|
findOne(where: Where[]): Promise<Record<string, unknown> | null>;
|
|
29
46
|
findMany(options?: FindManyOptions): Promise<Record<string, unknown>[]>;
|
|
30
|
-
count(where?: Where[]
|
|
47
|
+
count(where?: Where[] | {
|
|
48
|
+
where?: Where[];
|
|
49
|
+
filter?: FilterNode;
|
|
50
|
+
}): Promise<number>;
|
|
31
51
|
update(where: Where[], data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
32
52
|
updateMany(where: Where[], data: Record<string, unknown>): Promise<number>;
|
|
33
53
|
delete(where: Where[]): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-adapter.d.ts","sourceRoot":"","sources":["../../src/db/internal-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"internal-adapter.d.ts","sourceRoot":"","sources":["../../src/db/internal-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kysely-factory.d.ts","sourceRoot":"","sources":["../../src/db/kysely-factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,
|
|
1
|
+
{"version":3,"file":"kysely-factory.d.ts","sourceRoot":"","sources":["../../src/db/kysely-factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,MAAM,EAAgD,MAAM,QAAQ,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;AAEjE;;;;;;;;;GASG;AAEH,MAAM,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAErC;;;;GAIG;AACH,wBAAgB,kBAAkB,CACjC,EAAE,EAAE,kBAAkB,GACpB,kBAAkB,GAAG,IAAI,CA6B3B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,UAAU,EAAE,kBAAkB,GAC5B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAkDjC"}
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MountedService } from "../core/service";
|
|
2
2
|
export interface FieldDefinition {
|
|
3
|
-
type: "string" | "number" | "boolean" | "date" | "json";
|
|
3
|
+
type: "string" | "number" | "boolean" | "date" | "json" | "binary";
|
|
4
4
|
required?: boolean;
|
|
5
5
|
unique?: boolean;
|
|
6
6
|
primaryKey?: boolean;
|
package/dist/db/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;KAC/C,CAAC;CACF;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,cAAc,EAAE,GACxB,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CA2B5B;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { HostConfig, Host } from "./core/host";
|
|
|
5
5
|
export type { ServiceContext, Logger, HostInfo, ResolvedConfig, } from "./core/context";
|
|
6
6
|
export type { ServiceDBSchema, TableDefinition, FieldDefinition, PrefixedTable, } from "./db/schema";
|
|
7
7
|
export { getServiceTables, prefixTableName } from "./db/schema";
|
|
8
|
-
export type { InternalAdapter, TableAdapter, FindManyOptions, Where } from "./db/internal-adapter";
|
|
8
|
+
export type { InternalAdapter, TableAdapter, FindManyOptions, Where, } from "./db/internal-adapter";
|
|
9
9
|
export { createServiceEndpoint } from "./router/endpoint";
|
|
10
10
|
export { createServiceMiddleware } from "./router/middleware";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EACX,iBAAiB,EACjB,cAAc,EACd,aAAa,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EACX,cAAc,EACd,MAAM,EACN,QAAQ,EACR,cAAc,GACd,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACX,eAAe,EACf,eAAe,EACf,eAAe,EACf,aAAa,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EACX,iBAAiB,EACjB,cAAc,EACd,aAAa,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EACX,cAAc,EACd,MAAM,EACN,QAAQ,EACR,cAAc,GACd,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACX,eAAe,EACf,eAAe,EACf,eAAe,EACf,aAAa,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChE,YAAY,EACX,eAAe,EACf,YAAY,EACZ,eAAe,EACf,KAAK,GACL,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getServiceTables,
|
|
3
3
|
prefixTableName
|
|
4
4
|
} from "./chunk-nj3a3jzm.js";
|
|
5
|
-
import"./chunk-
|
|
5
|
+
import"./chunk-v0bahtg2.js";
|
|
6
6
|
|
|
7
7
|
// src/core/service.ts
|
|
8
8
|
function createService(definition) {
|
|
@@ -11,81 +11,6 @@ function createService(definition) {
|
|
|
11
11
|
mountConfig: config
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
-
// src/core/context.ts
|
|
15
|
-
function createLogger(serviceId) {
|
|
16
|
-
const prefix = `[futonic:${serviceId}]`;
|
|
17
|
-
return {
|
|
18
|
-
info: (msg, ...args) => console.info(prefix, msg, ...args),
|
|
19
|
-
warn: (msg, ...args) => console.warn(prefix, msg, ...args),
|
|
20
|
-
error: (msg, ...args) => console.error(prefix, msg, ...args),
|
|
21
|
-
debug: (msg, ...args) => console.debug(prefix, msg, ...args)
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// src/db/kysely-factory.ts
|
|
26
|
-
import {
|
|
27
|
-
Kysely,
|
|
28
|
-
MysqlDialect,
|
|
29
|
-
PostgresDialect,
|
|
30
|
-
SqliteDialect
|
|
31
|
-
} from "kysely";
|
|
32
|
-
function detectDatabaseType(db) {
|
|
33
|
-
if (!db)
|
|
34
|
-
return null;
|
|
35
|
-
if ("createDriver" in db) {
|
|
36
|
-
if (db instanceof SqliteDialect)
|
|
37
|
-
return "sqlite";
|
|
38
|
-
if (db instanceof MysqlDialect)
|
|
39
|
-
return "mysql";
|
|
40
|
-
if (db instanceof PostgresDialect)
|
|
41
|
-
return "postgres";
|
|
42
|
-
}
|
|
43
|
-
if ("aggregate" in db)
|
|
44
|
-
return "sqlite";
|
|
45
|
-
if ("getConnection" in db)
|
|
46
|
-
return "mysql";
|
|
47
|
-
if ("connect" in db)
|
|
48
|
-
return "postgres";
|
|
49
|
-
if ("fileControl" in db)
|
|
50
|
-
return "sqlite";
|
|
51
|
-
if ("open" in db && "close" in db && "prepare" in db)
|
|
52
|
-
return "sqlite";
|
|
53
|
-
if ("batch" in db && "exec" in db && "prepare" in db)
|
|
54
|
-
return "sqlite";
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
function createKyselyInstance(connection) {
|
|
58
|
-
if ("createDriver" in connection) {
|
|
59
|
-
return new Kysely({ dialect: connection });
|
|
60
|
-
}
|
|
61
|
-
if (typeof connection === "object" && connection !== null && "dialect" in connection && typeof connection.dialect === "object" && "createDriver" in connection.dialect) {
|
|
62
|
-
return new Kysely({ dialect: connection.dialect });
|
|
63
|
-
}
|
|
64
|
-
const dbType = detectDatabaseType(connection);
|
|
65
|
-
let dialect;
|
|
66
|
-
switch (dbType) {
|
|
67
|
-
case "sqlite": {
|
|
68
|
-
if ("fileControl" in connection) {
|
|
69
|
-
dialect = new SqliteDialect({ database: connection });
|
|
70
|
-
} else {
|
|
71
|
-
dialect = new SqliteDialect({ database: connection });
|
|
72
|
-
}
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case "mysql": {
|
|
76
|
-
dialect = new MysqlDialect({ pool: connection });
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case "postgres": {
|
|
80
|
-
dialect = new PostgresDialect({ pool: connection });
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
default:
|
|
84
|
-
throw new Error("Could not detect database type from the provided connection. " + "Pass a pg.Pool, mysql2 pool, better-sqlite3 instance, or a Kysely Dialect.");
|
|
85
|
-
}
|
|
86
|
-
return new Kysely({ dialect });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
14
|
// src/db/internal-adapter.ts
|
|
90
15
|
function createInternalAdapter(kysely, serviceId, schema) {
|
|
91
16
|
const allowedTables = new Set(schema ? Object.keys(schema.tables) : []);
|
|
@@ -131,6 +56,54 @@ function applyWhere(query, where) {
|
|
|
131
56
|
}
|
|
132
57
|
return query;
|
|
133
58
|
}
|
|
59
|
+
function buildFilter(eb, node) {
|
|
60
|
+
if (!node)
|
|
61
|
+
return eb.lit(true);
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case "and":
|
|
64
|
+
return node.nodes.length ? eb.and(node.nodes.map((n) => buildFilter(eb, n))) : eb.lit(true);
|
|
65
|
+
case "or":
|
|
66
|
+
return node.nodes.length ? eb.or(node.nodes.map((n) => buildFilter(eb, n))) : eb.lit(false);
|
|
67
|
+
case "not":
|
|
68
|
+
return eb.not(buildFilter(eb, node.node));
|
|
69
|
+
case "cond": {
|
|
70
|
+
const { field, op, value } = node;
|
|
71
|
+
const c = (sqlOp, val) => eb(field, sqlOp, val);
|
|
72
|
+
switch (op) {
|
|
73
|
+
case "eq":
|
|
74
|
+
return value === null ? c("is", null) : c("=", value);
|
|
75
|
+
case "ne":
|
|
76
|
+
return value === null ? c("is not", null) : c("<>", value);
|
|
77
|
+
case "gt":
|
|
78
|
+
return c(">", value);
|
|
79
|
+
case "gte":
|
|
80
|
+
return c(">=", value);
|
|
81
|
+
case "lt":
|
|
82
|
+
return c("<", value);
|
|
83
|
+
case "lte":
|
|
84
|
+
return c("<=", value);
|
|
85
|
+
case "in":
|
|
86
|
+
return c("in", value);
|
|
87
|
+
case "not_in":
|
|
88
|
+
return c("not in", value);
|
|
89
|
+
case "contains":
|
|
90
|
+
return c("like", `%${value}%`);
|
|
91
|
+
case "startsWith":
|
|
92
|
+
return c("like", `${value}%`);
|
|
93
|
+
case "endsWith":
|
|
94
|
+
return c("like", `%${value}`);
|
|
95
|
+
case "isNull":
|
|
96
|
+
return c("is", null);
|
|
97
|
+
case "isNotNull":
|
|
98
|
+
return c("is not", null);
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unsupported filter op: ${op}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`Unknown filter node type: ${node.type}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
134
107
|
function createTableAdapter(kysely, tableName) {
|
|
135
108
|
return {
|
|
136
109
|
async create(data) {
|
|
@@ -144,10 +117,13 @@ function createTableAdapter(kysely, tableName) {
|
|
|
144
117
|
return result ?? null;
|
|
145
118
|
},
|
|
146
119
|
async findMany(options) {
|
|
147
|
-
let query = kysely.selectFrom(tableName).selectAll();
|
|
120
|
+
let query = options?.select?.length ? kysely.selectFrom(tableName).select(options.select) : kysely.selectFrom(tableName).selectAll();
|
|
148
121
|
if (options?.where) {
|
|
149
122
|
query = applyWhere(query, options.where);
|
|
150
123
|
}
|
|
124
|
+
if (options?.filter) {
|
|
125
|
+
query = query.where((eb) => buildFilter(eb, options.filter));
|
|
126
|
+
}
|
|
151
127
|
if (options?.sortBy) {
|
|
152
128
|
query = query.orderBy(options.sortBy.field, options.sortBy.direction);
|
|
153
129
|
}
|
|
@@ -161,8 +137,13 @@ function createTableAdapter(kysely, tableName) {
|
|
|
161
137
|
},
|
|
162
138
|
async count(where) {
|
|
163
139
|
let query = kysely.selectFrom(tableName).select(kysely.fn.count("id").as("count"));
|
|
164
|
-
|
|
165
|
-
|
|
140
|
+
const whereArr = Array.isArray(where) ? where : where?.where;
|
|
141
|
+
const filter = Array.isArray(where) ? undefined : where?.filter;
|
|
142
|
+
if (whereArr) {
|
|
143
|
+
query = applyWhere(query, whereArr);
|
|
144
|
+
}
|
|
145
|
+
if (filter) {
|
|
146
|
+
query = query.where((eb) => buildFilter(eb, filter));
|
|
166
147
|
}
|
|
167
148
|
const res = await query.execute();
|
|
168
149
|
const count = res[0]?.count;
|
|
@@ -198,6 +179,76 @@ function createTableAdapter(kysely, tableName) {
|
|
|
198
179
|
};
|
|
199
180
|
}
|
|
200
181
|
|
|
182
|
+
// src/db/kysely-factory.ts
|
|
183
|
+
import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from "kysely";
|
|
184
|
+
function detectDatabaseType(db) {
|
|
185
|
+
if (!db)
|
|
186
|
+
return null;
|
|
187
|
+
if ("createDriver" in db) {
|
|
188
|
+
if (db instanceof SqliteDialect)
|
|
189
|
+
return "sqlite";
|
|
190
|
+
if (db instanceof MysqlDialect)
|
|
191
|
+
return "mysql";
|
|
192
|
+
if (db instanceof PostgresDialect)
|
|
193
|
+
return "postgres";
|
|
194
|
+
}
|
|
195
|
+
if ("aggregate" in db)
|
|
196
|
+
return "sqlite";
|
|
197
|
+
if ("getConnection" in db)
|
|
198
|
+
return "mysql";
|
|
199
|
+
if ("connect" in db)
|
|
200
|
+
return "postgres";
|
|
201
|
+
if ("fileControl" in db)
|
|
202
|
+
return "sqlite";
|
|
203
|
+
if ("open" in db && "close" in db && "prepare" in db)
|
|
204
|
+
return "sqlite";
|
|
205
|
+
if ("batch" in db && "exec" in db && "prepare" in db)
|
|
206
|
+
return "sqlite";
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
function createKyselyInstance(connection) {
|
|
210
|
+
if ("createDriver" in connection) {
|
|
211
|
+
return new Kysely({ dialect: connection });
|
|
212
|
+
}
|
|
213
|
+
if (typeof connection === "object" && connection !== null && "dialect" in connection && typeof connection.dialect === "object" && "createDriver" in connection.dialect) {
|
|
214
|
+
return new Kysely({ dialect: connection.dialect });
|
|
215
|
+
}
|
|
216
|
+
const dbType = detectDatabaseType(connection);
|
|
217
|
+
let dialect;
|
|
218
|
+
switch (dbType) {
|
|
219
|
+
case "sqlite": {
|
|
220
|
+
if ("fileControl" in connection) {
|
|
221
|
+
dialect = new SqliteDialect({ database: connection });
|
|
222
|
+
} else {
|
|
223
|
+
dialect = new SqliteDialect({ database: connection });
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "mysql": {
|
|
228
|
+
dialect = new MysqlDialect({ pool: connection });
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "postgres": {
|
|
232
|
+
dialect = new PostgresDialect({ pool: connection });
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
throw new Error("Could not detect database type from the provided connection. " + "Pass a pg.Pool, mysql2 pool, better-sqlite3 instance, or a Kysely Dialect.");
|
|
237
|
+
}
|
|
238
|
+
return new Kysely({ dialect });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/core/context.ts
|
|
242
|
+
function createLogger(serviceId) {
|
|
243
|
+
const prefix = `[futonic:${serviceId}]`;
|
|
244
|
+
return {
|
|
245
|
+
info: (msg, ...args) => console.info(prefix, msg, ...args),
|
|
246
|
+
warn: (msg, ...args) => console.warn(prefix, msg, ...args),
|
|
247
|
+
error: (msg, ...args) => console.error(prefix, msg, ...args),
|
|
248
|
+
debug: (msg, ...args) => console.debug(prefix, msg, ...args)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
// src/core/host.ts
|
|
202
253
|
function createHost(config) {
|
|
203
254
|
const services = new Map;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../src/router/endpoint.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../src/router/endpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAC9C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,eAAe,GAAG;IAAE,UAAU,CAAC,EAAE,cAAc,CAAA;CAAE,EAC1D,OAAO,EAAE,CAAC,GAAG,EAAE;IACd,OAAO,EAAE;QAAE,UAAU,EAAE,cAAc,CAAA;KAAE,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;CACjB,KAAK,OAAO,CAAC,SAAS,CAAC;;;;;;eAqBa,CAAC;mBAAiB,CAAC;uBAA2B,CAAC;gBAAoB,CAAC;uBAA6B,CAAC;sBAA0B,CAAC;uBAAuC,CAAC;;;;gCAA+F,CAAC;sCAA6C,CAAC;oCAA6C,CAAC;gCAA8B,CAAC;;;;;qBAAyE,CAAC;;;2BAAgF,CAAC;0CAAkC,CAAC;;oCAAgD,CAAC;0CAA+C,CAAC;wCAA+C,CAAC;oCAAgC,CAAC;;;oCAAkE,CAAC;kCAAwB,CAAC;oCAAwB,CAAC;0CAA+C,CAAC;wCAA+C,CAAC;oCAAgC,CAAC;;;mCAAiE,CAAC;kCAAwB,CAAC;oCAAwB,CAAC;0CAA+C,CAAC;wCAA+C,CAAC;oCAAgC,CAAC;;;;;;;cAA6jB,CAAC;gBAAgD,CAAC;iBAAqD,CAAC;;mBAA2K,CAAC;gBAAkK,CAAC;aAA4b,CAAC;yBAA4f,CAAC;;;;;;;;;;;;;;;;;;;;;;;;YAD9qG"}
|
package/package.json
CHANGED
|
@@ -1,64 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
2
|
+
"name": "futonic",
|
|
3
|
+
"version": "0.1.1-canary.e9561d3",
|
|
4
|
+
"description": "Embed API routes and database tables into any host application",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/isaacwasserman/futonic.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"import": "./dist/client/index.js",
|
|
19
|
+
"types": "./dist/client/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./cli": {
|
|
22
|
+
"import": "./dist/cli/index.js",
|
|
23
|
+
"types": "./dist/cli/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "bun run scripts/build.ts",
|
|
28
|
+
"dev": "bun --watch src/index.ts",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "biome check src/",
|
|
32
|
+
"format": "biome format --write src/"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"better-call": "^1.0.4",
|
|
36
|
+
"kysely": "^0.27.6",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "^1.9.4",
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"better-sqlite3": "^11.0.0",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
},
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"@mrleebo/prisma-ast": "^0.12.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"pg": ">=8.0.0",
|
|
52
|
+
"mysql2": ">=3.0.0",
|
|
53
|
+
"better-sqlite3": ">=9.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"pg": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"mysql2": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"better-sqlite3": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"dist"
|
|
68
|
+
],
|
|
69
|
+
"license": "MIT"
|
|
64
70
|
}
|
package/dist/chunk-5va59f7m.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
-
|
|
20
|
-
export { __toESM, __require };
|