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 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
- __toESM
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 refModelName = toPascalCase(field.references.model);
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(field.references.model.toLowerCase(), `${refModelName}${field.required ? "" : "?"}`).attribute(relationAttr);
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-5va59f7m.js";
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
- columns.push(` FOREIGN KEY (${fieldName}) REFERENCES ${field.references.model}(${field.references.field}) ON DELETE ${action}`);
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-5va59f7m.js";
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
- col += `.references(() => ${field.references.model}.${field.references.field}, { onDelete: '${action}' })`;
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(`,
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ export { __require };
@@ -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,CAoBR"}
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,eAyHlC,CAAC"}
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
- __toESM
7
- } from "../chunk-5va59f7m.js";
5
+ __require
6
+ } from "../chunk-v0bahtg2.js";
8
7
 
9
8
  // src/cli/index.ts
10
- import { writeFile, mkdir } from "node:fs/promises";
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-q65vm76v.js");
82
+ const { generateDrizzleSchema } = await import("../chunk-s9sdjw1m.js");
84
83
  return generateDrizzleSchema;
85
84
  }
86
85
  case "prisma": {
87
- const { generatePrismaSchema } = await import("../chunk-j2yky04y.js");
86
+ const { generatePrismaSchema } = await import("../chunk-eqeddyek.js");
88
87
  return generatePrismaSchema;
89
88
  }
90
89
  case "kysely": {
91
- const { generateKyselySchema } = await import("../chunk-6w1vqkpr.js");
90
+ const { generateKyselySchema } = await import("../chunk-s3j6ndfy.js");
92
91
  return generateKyselySchema;
93
92
  }
94
93
  default:
@@ -1,4 +1,4 @@
1
- import"../chunk-5va59f7m.js";
1
+ import"../chunk-v0bahtg2.js";
2
2
 
3
3
  // src/client/index.ts
4
4
  import { createClient } from "better-call/client";
@@ -1,5 +1,5 @@
1
- import type { MountedService } from "./service";
2
1
  import { type DatabaseConnection } from "../db/kysely-factory";
2
+ import type { MountedService } from "./service";
3
3
  export interface HostConfig {
4
4
  database?: DatabaseConnection;
5
5
  services: MountedService[];
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/core/host.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAIrF,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,CA2EnD"}
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[]): Promise<number>;
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,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAEjE,MAAM,WAAW,KAAK;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAC;IACvE,SAAS,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;CACtD;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACjE,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACxE,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,CACL,KAAK,EAAE,KAAK,EAAE,EACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3E,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,eAAe,GAAG,eAAe,IAC5E;KACE,CAAC,IAAI,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,YAAY;CAC5C,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,eAAe,EACpE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,GACd,eAAe,CAAC,OAAO,CAAC,CAe1B"}
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,EACN,MAAM,EAIN,MAAM,QAAQ,CAAC;AAEhB,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"}
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"}
@@ -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;
@@ -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;IACxD,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"}
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
@@ -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,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC"}
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-5va59f7m.js";
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
- if (where) {
165
- query = applyWhere(query, where);
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,EAAkB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;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;IAAE,OAAO,EAAE;QAAE,UAAU,EAAE,cAAc,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,SAAS,CAAC;;;;;;eAiBpH,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;;;;;;;;;;;;;;;;;;;;;;;;YADvsG"}
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
- "name": "futonic",
3
- "version": "0.1.0",
4
- "description": "Embed API routes and database tables into any host application",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- },
13
- "./client": {
14
- "import": "./dist/client/index.js",
15
- "types": "./dist/client/index.d.ts"
16
- },
17
- "./cli": {
18
- "import": "./dist/cli/index.js",
19
- "types": "./dist/cli/index.d.ts"
20
- }
21
- },
22
- "scripts": {
23
- "build": "bun run scripts/build.ts",
24
- "dev": "bun --watch src/index.ts",
25
- "test": "bun test",
26
- "typecheck": "tsc --noEmit",
27
- "lint": "biome check src/",
28
- "format": "biome format --write src/"
29
- },
30
- "dependencies": {
31
- "better-call": "^1.0.4",
32
- "kysely": "^0.27.6",
33
- "zod": "^4.3.6"
34
- },
35
- "devDependencies": {
36
- "@biomejs/biome": "^1.9.4",
37
- "@types/better-sqlite3": "^7.6.13",
38
- "@types/bun": "latest",
39
- "better-sqlite3": "^11.0.0",
40
- "tsx": "^4.21.0",
41
- "typescript": "^5.7.0"
42
- },
43
- "optionalDependencies": {
44
- "@mrleebo/prisma-ast": "^0.12.0"
45
- },
46
- "peerDependencies": {
47
- "pg": ">=8.0.0",
48
- "mysql2": ">=3.0.0",
49
- "better-sqlite3": ">=9.0.0"
50
- },
51
- "peerDependenciesMeta": {
52
- "pg": {
53
- "optional": true
54
- },
55
- "mysql2": {
56
- "optional": true
57
- },
58
- "better-sqlite3": {
59
- "optional": true
60
- }
61
- },
62
- "files": ["dist"],
63
- "license": "MIT"
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
  }
@@ -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 };