futonic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/chunk-5va59f7m.js +20 -0
  2. package/dist/chunk-6w1vqkpr.js +87 -0
  3. package/dist/chunk-j2yky04y.js +130 -0
  4. package/dist/chunk-nj3a3jzm.js +26 -0
  5. package/dist/chunk-q65vm76v.js +180 -0
  6. package/dist/cli/generators/drizzle.d.ts +11 -0
  7. package/dist/cli/generators/drizzle.d.ts.map +1 -0
  8. package/dist/cli/generators/kysely.d.ts +17 -0
  9. package/dist/cli/generators/kysely.d.ts.map +1 -0
  10. package/dist/cli/generators/prisma.d.ts +10 -0
  11. package/dist/cli/generators/prisma.d.ts.map +1 -0
  12. package/dist/cli/generators/types.d.ts +24 -0
  13. package/dist/cli/generators/types.d.ts.map +1 -0
  14. package/dist/cli/index.d.ts +28 -0
  15. package/dist/cli/index.d.ts.map +1 -0
  16. package/dist/cli/index.js +101 -0
  17. package/dist/client/index.d.ts +15 -0
  18. package/dist/client/index.d.ts.map +1 -0
  19. package/dist/client/index.js +7 -0
  20. package/dist/core/context.d.ts +23 -0
  21. package/dist/core/context.d.ts.map +1 -0
  22. package/dist/core/host.d.ts +14 -0
  23. package/dist/core/host.d.ts.map +1 -0
  24. package/dist/core/service.d.ts +25 -0
  25. package/dist/core/service.d.ts.map +1 -0
  26. package/dist/db/internal-adapter.d.ts +43 -0
  27. package/dist/db/internal-adapter.d.ts.map +1 -0
  28. package/dist/db/kysely-factory.d.ts +34 -0
  29. package/dist/db/kysely-factory.d.ts.map +1 -0
  30. package/dist/db/schema.d.ts +36 -0
  31. package/dist/db/schema.d.ts.map +1 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +286 -0
  35. package/dist/router/endpoint.d.ts +105 -0
  36. package/dist/router/endpoint.d.ts.map +1 -0
  37. package/dist/router/middleware.d.ts +10 -0
  38. package/dist/router/middleware.d.ts.map +1 -0
  39. package/dist/test-utils.d.ts +25 -0
  40. package/dist/test-utils.d.ts.map +1 -0
  41. package/package.json +64 -0
@@ -0,0 +1,7 @@
1
+ import"../chunk-5va59f7m.js";
2
+
3
+ // src/client/index.ts
4
+ import { createClient } from "better-call/client";
5
+ export {
6
+ createClient
7
+ };
@@ -0,0 +1,23 @@
1
+ import type { InternalAdapter } from "../db/internal-adapter";
2
+ import type { ServiceDBSchema } from "../db/schema";
3
+ export interface Logger {
4
+ info(message: string, ...args: unknown[]): void;
5
+ warn(message: string, ...args: unknown[]): void;
6
+ error(message: string, ...args: unknown[]): void;
7
+ debug(message: string, ...args: unknown[]): void;
8
+ }
9
+ export interface HostInfo {
10
+ baseURL: string;
11
+ mountPath: string;
12
+ }
13
+ export interface ResolvedConfig {
14
+ [key: string]: unknown;
15
+ }
16
+ export interface ServiceContext<TSchema extends ServiceDBSchema = ServiceDBSchema> {
17
+ db: InternalAdapter<TSchema>;
18
+ config: ResolvedConfig;
19
+ logger: Logger;
20
+ hostInfo: HostInfo;
21
+ }
22
+ export declare function createLogger(serviceId: string): Logger;
23
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,WAAW,MAAM;IACtB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,QAAQ;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc,CAC9B,OAAO,SAAS,eAAe,GAAG,eAAe;IAEjD,EAAE,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQtD"}
@@ -0,0 +1,14 @@
1
+ import type { MountedService } from "./service";
2
+ import { type DatabaseConnection } from "../db/kysely-factory";
3
+ export interface HostConfig {
4
+ database?: DatabaseConnection;
5
+ services: MountedService[];
6
+ baseURL?: string;
7
+ }
8
+ export interface Host {
9
+ services: Map<string, MountedService>;
10
+ init(): Promise<void>;
11
+ shutdown(): Promise<void>;
12
+ }
13
+ export declare function createHost(config: HostConfig): Host;
14
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,25 @@
1
+ import type { ServiceDBSchema } from "../db/schema";
2
+ import type { ServiceContext } from "./context";
3
+ export interface ServiceConfig<TConfig = unknown> {
4
+ mount: string;
5
+ middleware?: unknown[];
6
+ config?: TConfig;
7
+ }
8
+ export interface EmbeddableService<TConfig = unknown, TSchema extends ServiceDBSchema = ServiceDBSchema, TEndpoints extends Record<string, unknown> = Record<string, unknown>> {
9
+ id: string;
10
+ version: string;
11
+ dependencies: {
12
+ database: boolean;
13
+ };
14
+ dbSchema?: TSchema;
15
+ endpoints: TEndpoints;
16
+ onInit?: (ctx: ServiceContext<TSchema>) => Promise<void>;
17
+ onReady?: (ctx: ServiceContext<TSchema>) => Promise<void>;
18
+ onShutdown?: () => Promise<void>;
19
+ }
20
+ export interface MountedService<TConfig = unknown, TSchema extends ServiceDBSchema = ServiceDBSchema, TEndpoints extends Record<string, unknown> = Record<string, unknown>> extends EmbeddableService<TConfig, TSchema, TEndpoints> {
21
+ mountConfig: ServiceConfig<TConfig>;
22
+ serviceContext?: ServiceContext<TSchema>;
23
+ }
24
+ export declare function createService<TConfig, TSchema extends ServiceDBSchema, TEndpoints extends Record<string, unknown>>(definition: EmbeddableService<TConfig, TSchema, TEndpoints>): (config: ServiceConfig<TConfig>) => MountedService<TConfig, TSchema, TEndpoints>;
25
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/core/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,aAAa,CAAC,OAAO,GAAG,OAAO;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB,CACjC,OAAO,GAAG,OAAO,EACjB,OAAO,SAAS,eAAe,GAAG,eAAe,EACjD,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEpE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QACb,QAAQ,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,UAAU,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc,CAC9B,OAAO,GAAG,OAAO,EACjB,OAAO,SAAS,eAAe,GAAG,eAAe,EACjD,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACnE,SAAQ,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC;IACxD,WAAW,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,wBAAgB,aAAa,CAC5B,OAAO,EACP,OAAO,SAAS,eAAe,EAC/B,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAE1C,UAAU,EAAE,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,GACzD,CACF,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,KAC1B,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAKhD"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Internal database adapter that scopes all queries to a service's prefixed tables.
3
+ *
4
+ * Follows the same 8-method interface as better-auth's Kysely adapter
5
+ * (`create`, `findOne`, `findMany`, `count`, `update`, `updateMany`, `delete`, `deleteMany`).
6
+ *
7
+ * Access tables via property name: `db.invoices.create({ ... })`.
8
+ */
9
+ import type { Kysely } from "kysely";
10
+ import { type ServiceDBSchema } from "./schema";
11
+ export interface Where {
12
+ field: string;
13
+ value: unknown;
14
+ operator?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "not_in";
15
+ connector?: "AND" | "OR";
16
+ }
17
+ export interface FindManyOptions {
18
+ where?: Where[];
19
+ limit?: number;
20
+ offset?: number;
21
+ sortBy?: {
22
+ field: string;
23
+ direction: "asc" | "desc";
24
+ };
25
+ }
26
+ export interface TableAdapter {
27
+ create(data: Record<string, unknown>): Promise<Record<string, unknown>>;
28
+ findOne(where: Where[]): Promise<Record<string, unknown> | null>;
29
+ findMany(options?: FindManyOptions): Promise<Record<string, unknown>[]>;
30
+ count(where?: Where[]): Promise<number>;
31
+ update(where: Where[], data: Record<string, unknown>): Promise<Record<string, unknown>>;
32
+ updateMany(where: Where[], data: Record<string, unknown>): Promise<number>;
33
+ delete(where: Where[]): Promise<void>;
34
+ deleteMany(where: Where[]): Promise<number>;
35
+ }
36
+ /**
37
+ * Proxy-based adapter that scopes all queries to a service's prefixed tables.
38
+ */
39
+ export type InternalAdapter<TSchema extends ServiceDBSchema = ServiceDBSchema> = {
40
+ [K in keyof TSchema["tables"]]: TableAdapter;
41
+ };
42
+ export declare function createInternalAdapter<TSchema extends ServiceDBSchema>(kysely: Kysely<Record<string, unknown>>, serviceId: string, schema?: TSchema): InternalAdapter<TSchema>;
43
+ //# sourceMappingURL=internal-adapter.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Kysely instance factory with dialect auto-detection.
3
+ *
4
+ * Forked from better-auth's `packages/kysely-adapter/src/dialect.ts` (MIT).
5
+ * Adapted to work standalone without BetterAuthOptions — accepts raw driver
6
+ * instances or Kysely Dialect objects directly.
7
+ */
8
+ import { Kysely } from "kysely";
9
+ export type KyselyDatabaseType = "sqlite" | "mysql" | "postgres";
10
+ /**
11
+ * The host passes one of these to `createHost({ database: ... })`.
12
+ *
13
+ * Accepted forms:
14
+ * - A Kysely `Dialect` instance (most explicit)
15
+ * - A `pg.Pool` (detected via `"connect"` property)
16
+ * - A `mysql2` pool (detected via `"getConnection"` property)
17
+ * - A `better-sqlite3` instance (detected via `"aggregate"` property)
18
+ * - A Bun SQLite instance (detected via `"fileControl"` property)
19
+ */
20
+ export type DatabaseConnection = any;
21
+ /**
22
+ * Detects the database type from a driver instance using property sniffing.
23
+ *
24
+ * Forked from better-auth `getKyselyDatabaseType()`.
25
+ */
26
+ export declare function detectDatabaseType(db: DatabaseConnection): KyselyDatabaseType | null;
27
+ /**
28
+ * Creates a Kysely instance from the host's database connection.
29
+ *
30
+ * Forked from better-auth `createKyselyAdapter()`.
31
+ * Auto-detects the dialect from the driver instance shape.
32
+ */
33
+ export declare function createKyselyInstance(connection: DatabaseConnection): Kysely<Record<string, unknown>>;
34
+ //# sourceMappingURL=kysely-factory.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,36 @@
1
+ import type { MountedService } from "../core/service";
2
+ export interface FieldDefinition {
3
+ type: "string" | "number" | "boolean" | "date" | "json";
4
+ required?: boolean;
5
+ unique?: boolean;
6
+ primaryKey?: boolean;
7
+ defaultValue?: unknown;
8
+ enum?: string[];
9
+ references?: {
10
+ model: string;
11
+ field: string;
12
+ onDelete?: "cascade" | "restrict" | "set-null";
13
+ };
14
+ }
15
+ export interface TableDefinition {
16
+ fields: Record<string, FieldDefinition>;
17
+ }
18
+ export interface ServiceDBSchema {
19
+ tables: Record<string, TableDefinition>;
20
+ }
21
+ export interface PrefixedTable {
22
+ originalName: string;
23
+ prefixedName: string;
24
+ serviceId: string;
25
+ fields: Record<string, FieldDefinition>;
26
+ }
27
+ /**
28
+ * Collects all mounted services' dbSchema values, applies `{serviceId}_` prefix,
29
+ * and returns a unified map of prefixed table name → table definition.
30
+ */
31
+ export declare function getServiceTables(services: MountedService[]): Map<string, PrefixedTable>;
32
+ /**
33
+ * Maps a logical (unprefixed) table name to its prefixed counterpart.
34
+ */
35
+ export declare function prefixTableName(serviceId: string, tableName: string): string;
36
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,11 @@
1
+ export { createService } from "./core/service";
2
+ export type { EmbeddableService, MountedService, ServiceConfig, } from "./core/service";
3
+ export { createHost } from "./core/host";
4
+ export type { HostConfig, Host } from "./core/host";
5
+ export type { ServiceContext, Logger, HostInfo, ResolvedConfig, } from "./core/context";
6
+ export type { ServiceDBSchema, TableDefinition, FieldDefinition, PrefixedTable, } from "./db/schema";
7
+ export { getServiceTables, prefixTableName } from "./db/schema";
8
+ export type { InternalAdapter, TableAdapter, FindManyOptions, Where } from "./db/internal-adapter";
9
+ export { createServiceEndpoint } from "./router/endpoint";
10
+ export { createServiceMiddleware } from "./router/middleware";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ import {
2
+ getServiceTables,
3
+ prefixTableName
4
+ } from "./chunk-nj3a3jzm.js";
5
+ import"./chunk-5va59f7m.js";
6
+
7
+ // src/core/service.ts
8
+ function createService(definition) {
9
+ return (config) => ({
10
+ ...definition,
11
+ mountConfig: config
12
+ });
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
+ // src/db/internal-adapter.ts
90
+ function createInternalAdapter(kysely, serviceId, schema) {
91
+ const allowedTables = new Set(schema ? Object.keys(schema.tables) : []);
92
+ return new Proxy({}, {
93
+ get(_target, prop) {
94
+ if (!allowedTables.has(prop)) {
95
+ throw new Error(`Service "${serviceId}" does not have a table named "${prop}"`);
96
+ }
97
+ const prefixed = prefixTableName(serviceId, prop);
98
+ return createTableAdapter(kysely, prefixed);
99
+ }
100
+ });
101
+ }
102
+ function applyWhere(query, where) {
103
+ for (const condition of where) {
104
+ const { field, value, operator = "eq" } = condition;
105
+ switch (operator) {
106
+ case "eq":
107
+ query = value === null ? query.where(field, "is", null) : query.where(field, "=", value);
108
+ break;
109
+ case "ne":
110
+ query = value === null ? query.where(field, "is not", null) : query.where(field, "<>", value);
111
+ break;
112
+ case "gt":
113
+ query = query.where(field, ">", value);
114
+ break;
115
+ case "gte":
116
+ query = query.where(field, ">=", value);
117
+ break;
118
+ case "lt":
119
+ query = query.where(field, "<", value);
120
+ break;
121
+ case "lte":
122
+ query = query.where(field, "<=", value);
123
+ break;
124
+ case "in":
125
+ query = query.where(field, "in", value);
126
+ break;
127
+ case "not_in":
128
+ query = query.where(field, "not in", value);
129
+ break;
130
+ }
131
+ }
132
+ return query;
133
+ }
134
+ function createTableAdapter(kysely, tableName) {
135
+ return {
136
+ async create(data) {
137
+ const result = await kysely.insertInto(tableName).values(data).returningAll().executeTakeFirstOrThrow();
138
+ return result;
139
+ },
140
+ async findOne(where) {
141
+ let query = kysely.selectFrom(tableName).selectAll();
142
+ query = applyWhere(query, where);
143
+ const result = await query.executeTakeFirst();
144
+ return result ?? null;
145
+ },
146
+ async findMany(options) {
147
+ let query = kysely.selectFrom(tableName).selectAll();
148
+ if (options?.where) {
149
+ query = applyWhere(query, options.where);
150
+ }
151
+ if (options?.sortBy) {
152
+ query = query.orderBy(options.sortBy.field, options.sortBy.direction);
153
+ }
154
+ if (options?.limit !== undefined) {
155
+ query = query.limit(options.limit);
156
+ }
157
+ if (options?.offset !== undefined) {
158
+ query = query.offset(options.offset);
159
+ }
160
+ return await query.execute();
161
+ },
162
+ async count(where) {
163
+ let query = kysely.selectFrom(tableName).select(kysely.fn.count("id").as("count"));
164
+ if (where) {
165
+ query = applyWhere(query, where);
166
+ }
167
+ const res = await query.execute();
168
+ const count = res[0]?.count;
169
+ if (typeof count === "number")
170
+ return count;
171
+ if (typeof count === "bigint")
172
+ return Number(count);
173
+ return Number.parseInt(count);
174
+ },
175
+ async update(where, data) {
176
+ let query = kysely.updateTable(tableName).set(data);
177
+ query = applyWhere(query, where);
178
+ const result = await query.returningAll().executeTakeFirstOrThrow();
179
+ return result;
180
+ },
181
+ async updateMany(where, data) {
182
+ let query = kysely.updateTable(tableName).set(data);
183
+ query = applyWhere(query, where);
184
+ const res = (await query.executeTakeFirst()).numUpdatedRows;
185
+ return res > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : Number(res);
186
+ },
187
+ async delete(where) {
188
+ let query = kysely.deleteFrom(tableName);
189
+ query = applyWhere(query, where);
190
+ await query.execute();
191
+ },
192
+ async deleteMany(where) {
193
+ let query = kysely.deleteFrom(tableName);
194
+ query = applyWhere(query, where);
195
+ const res = (await query.executeTakeFirst()).numDeletedRows;
196
+ return res > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : Number(res);
197
+ }
198
+ };
199
+ }
200
+
201
+ // src/core/host.ts
202
+ function createHost(config) {
203
+ const services = new Map;
204
+ for (const service of config.services) {
205
+ if (services.has(service.id)) {
206
+ throw new Error(`Namespace collision: service id "${service.id}" is used by multiple services`);
207
+ }
208
+ services.set(service.id, service);
209
+ }
210
+ const _allTables = getServiceTables(config.services);
211
+ let kyselyInstance;
212
+ return {
213
+ services,
214
+ async init() {
215
+ const needsDb = config.services.some((s) => s.dependencies.database);
216
+ if (needsDb) {
217
+ if (!config.database) {
218
+ throw new Error("At least one service requires a database, but no database connection was provided");
219
+ }
220
+ kyselyInstance = createKyselyInstance(config.database);
221
+ }
222
+ for (const service of config.services) {
223
+ const logger = createLogger(service.id);
224
+ const serviceCtx = {
225
+ db: service.dependencies.database && kyselyInstance ? createInternalAdapter(kyselyInstance, service.id, service.dbSchema) : undefined,
226
+ config: service.mountConfig.config ?? {},
227
+ logger,
228
+ hostInfo: {
229
+ baseURL: config.baseURL ?? "",
230
+ mountPath: service.mountConfig.mount
231
+ }
232
+ };
233
+ service.serviceContext = serviceCtx;
234
+ if (service.onInit) {
235
+ await service.onInit(serviceCtx);
236
+ }
237
+ }
238
+ for (const service of config.services) {
239
+ if (service.onReady && service.serviceContext) {
240
+ await service.onReady(service.serviceContext);
241
+ }
242
+ }
243
+ },
244
+ async shutdown() {
245
+ for (const service of config.services) {
246
+ if (service.onShutdown) {
247
+ await service.onShutdown();
248
+ }
249
+ }
250
+ if (kyselyInstance) {
251
+ await kyselyInstance.destroy();
252
+ }
253
+ }
254
+ };
255
+ }
256
+ // src/router/endpoint.ts
257
+ import { createEndpoint } from "better-call";
258
+
259
+ // src/router/middleware.ts
260
+ import { createMiddleware } from "better-call";
261
+ function createServiceMiddleware(serviceCtx) {
262
+ return createMiddleware(async () => {
263
+ return { serviceCtx };
264
+ });
265
+ }
266
+
267
+ // src/router/endpoint.ts
268
+ function createServiceEndpoint(path, options, handler) {
269
+ const middlewares = [...options.use ?? []];
270
+ if (options.serviceCtx) {
271
+ middlewares.unshift(createServiceMiddleware(options.serviceCtx));
272
+ }
273
+ const { serviceCtx: _, ...restOptions } = options;
274
+ return createEndpoint(path, {
275
+ ...restOptions,
276
+ use: middlewares
277
+ }, handler);
278
+ }
279
+ export {
280
+ prefixTableName,
281
+ getServiceTables,
282
+ createServiceMiddleware,
283
+ createServiceEndpoint,
284
+ createService,
285
+ createHost
286
+ };
@@ -0,0 +1,105 @@
1
+ import { type EndpointOptions } from "better-call";
2
+ import type { ServiceContext } from "../core/context";
3
+ /**
4
+ * Creates a service endpoint that automatically injects the ServiceContext
5
+ * middleware. Wraps better-call's `createEndpoint`.
6
+ */
7
+ export declare function createServiceEndpoint<TResponse>(path: string, options: EndpointOptions & {
8
+ serviceCtx?: ServiceContext;
9
+ }, handler: (ctx: {
10
+ context: {
11
+ serviceCtx: ServiceContext;
12
+ };
13
+ query: Record<string, unknown>;
14
+ body: unknown;
15
+ params: Record<string, string>;
16
+ headers: Headers;
17
+ }) => Promise<TResponse>): import("better-call").StrictEndpoint<string, ({
18
+ requireHeaders?: boolean | undefined;
19
+ requireRequest?: boolean | undefined;
20
+ cloneRequest?: boolean | undefined;
21
+ disableBody?: boolean | undefined;
22
+ metadata?: {
23
+ openapi?: {
24
+ summary?: string;
25
+ description?: string;
26
+ tags?: string[];
27
+ operationId?: string;
28
+ parameters?: import("better-call").OpenAPIParameter[];
29
+ requestBody?: {
30
+ content: {
31
+ "application/json": {
32
+ schema: {
33
+ type?: import("better-call").OpenAPISchemaType;
34
+ properties?: Record<string, any>;
35
+ required?: string[];
36
+ $ref?: string;
37
+ };
38
+ };
39
+ };
40
+ };
41
+ responses?: {
42
+ [status: string]: {
43
+ description: string;
44
+ content?: {
45
+ "application/json"?: {
46
+ schema: {
47
+ type?: import("better-call").OpenAPISchemaType;
48
+ properties?: Record<string, any>;
49
+ required?: string[];
50
+ $ref?: string;
51
+ };
52
+ };
53
+ "text/plain"?: {
54
+ schema?: {
55
+ type?: import("better-call").OpenAPISchemaType;
56
+ properties?: Record<string, any>;
57
+ required?: string[];
58
+ $ref?: string;
59
+ };
60
+ };
61
+ "text/html"?: {
62
+ schema?: {
63
+ type?: import("better-call").OpenAPISchemaType;
64
+ properties?: Record<string, any>;
65
+ required?: string[];
66
+ $ref?: string;
67
+ };
68
+ };
69
+ };
70
+ };
71
+ };
72
+ };
73
+ $Infer?: {
74
+ body?: any;
75
+ query?: Record<string, any>;
76
+ };
77
+ SERVER_ONLY?: boolean;
78
+ isAction?: boolean;
79
+ scope?: "rpc" | "server" | "http";
80
+ allowedMediaTypes?: string[];
81
+ [key: string]: any;
82
+ } | undefined;
83
+ use: any[];
84
+ onAPIError?: ((e: import("better-call").APIError) => void | Promise<void>) | undefined;
85
+ onValidationError?: (({ issues, message }: {
86
+ message: string;
87
+ issues: readonly import("better-call").StandardSchemaV1.Issue[];
88
+ }) => void | Promise<void>) | undefined;
89
+ } & ({
90
+ method: "POST" | "PUT" | "DELETE" | "PATCH" | ("POST" | "PUT" | "DELETE" | "PATCH")[];
91
+ body: import("better-call").StandardSchemaV1<unknown, unknown>;
92
+ } | {
93
+ method: "GET" | "HEAD" | ("GET" | "HEAD")[];
94
+ } | {
95
+ method: "*";
96
+ body?: import("better-call").StandardSchemaV1<unknown, unknown> | undefined;
97
+ } | {
98
+ method: ("POST" | "PUT" | "DELETE" | "PATCH" | "GET" | "HEAD")[];
99
+ body: import("better-call").StandardSchemaV1<unknown, unknown>;
100
+ })) & {
101
+ query: import("better-call").StandardSchemaV1<unknown, unknown>;
102
+ } & {
103
+ error: import("better-call").StandardSchemaV1<unknown, unknown>;
104
+ }, unknown>;
105
+ //# sourceMappingURL=endpoint.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,10 @@
1
+ import type { ServiceContext } from "../core/context";
2
+ /**
3
+ * Creates a better-call middleware that injects the ServiceContext
4
+ * onto `ctx.context.serviceCtx`, identical to how better-auth
5
+ * injects AuthContext.
6
+ */
7
+ export declare function createServiceMiddleware(serviceCtx: ServiceContext): <InputCtx extends import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>>(inputContext: InputCtx) => Promise<{
8
+ serviceCtx: ServiceContext<import("..").ServiceDBSchema>;
9
+ }>;
10
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/router/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,cAAc;;GAIjE"}