@parlor/data 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cychien
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,16 @@
1
- # Temporary Holding Version
1
+ # @parlor/data
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The app's PostgreSQL database through Drizzle: embedded PGlite in development, `DATABASE_URL` in production, migrations applied on start.
4
+
5
+ ```sh
6
+ pnpm add @parlor/data drizzle-orm
7
+ ```
8
+
9
+ ```ts
10
+ import { createDatabase } from "@parlor/data";
11
+
12
+ const database = await createDatabase({ migrationsFolder: "migrations" });
13
+ const rows = await database.db.execute("select 1");
14
+ ```
15
+
16
+ Docs: https://cychien.github.io/parlor/guides/data/
@@ -0,0 +1,25 @@
1
+ export interface ScopedConnections {
2
+ close(): Promise<void>;
3
+ }
4
+ /**
5
+ * Who owns the database connections opened while `run` executes. Outside every scope a
6
+ * handle keeps its own connections for the life of the process.
7
+ */
8
+ export interface ConnectionScope {
9
+ run<T>(fn: () => T): T;
10
+ close(): Promise<void>;
11
+ }
12
+ export declare class ConnectionOwner {
13
+ #private;
14
+ connectionsFor<C extends ScopedConnections>(key: object, open: () => C): C;
15
+ forget(key: object): void;
16
+ close(): Promise<void>;
17
+ }
18
+ export declare function currentOwner(): ConnectionOwner | undefined;
19
+ /**
20
+ * Connections opened inside `run` belong to this scope and close with it. Use one per
21
+ * runtime context that must not share sockets: a Worker request, or a Durable Object
22
+ * instance, since workerd cancels I/O started in one context and awaited from another.
23
+ */
24
+ export declare function createConnectionScope(): ConnectionScope;
25
+ //# sourceMappingURL=connection-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-scope.d.ts","sourceRoot":"","sources":["../src/connection-scope.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,qBAAa,eAAe;;IAI1B,cAAc,CAAC,CAAC,SAAS,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzE;IAED,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAExB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;CACF;AAID,wBAAgB,YAAY,IAAI,eAAe,GAAG,SAAS,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAMvD"}
@@ -0,0 +1,41 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export class ConnectionOwner {
3
+ #connections = new Map();
4
+ #closed = false;
5
+ connectionsFor(key, open) {
6
+ if (this.#closed)
7
+ throw new Error("This connection scope is closed.");
8
+ let connections = this.#connections.get(key);
9
+ if (!connections) {
10
+ connections = open();
11
+ this.#connections.set(key, connections);
12
+ }
13
+ return connections;
14
+ }
15
+ forget(key) {
16
+ this.#connections.delete(key);
17
+ }
18
+ async close() {
19
+ this.#closed = true;
20
+ const open = [...this.#connections.values()];
21
+ this.#connections.clear();
22
+ await Promise.all(open.map((connections) => connections.close()));
23
+ }
24
+ }
25
+ const active = new AsyncLocalStorage();
26
+ export function currentOwner() {
27
+ return active.getStore();
28
+ }
29
+ /**
30
+ * Connections opened inside `run` belong to this scope and close with it. Use one per
31
+ * runtime context that must not share sockets: a Worker request, or a Durable Object
32
+ * instance, since workerd cancels I/O started in one context and awaited from another.
33
+ */
34
+ export function createConnectionScope() {
35
+ const owner = new ConnectionOwner();
36
+ return {
37
+ run: (fn) => active.run(owner, fn),
38
+ close: () => owner.close(),
39
+ };
40
+ }
41
+ //# sourceMappingURL=connection-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-scope.js","sourceRoot":"","sources":["../src/connection-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAerD,MAAM,OAAO,eAAe;IACjB,YAAY,GAAG,IAAI,GAAG,EAA6B,CAAC;IAC7D,OAAO,GAAG,KAAK,CAAC;IAEhB,cAAc,CAA8B,GAAW,EAAE,IAAa;QACpE,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACtE,IAAI,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,WAAW,GAAG,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,WAAgB,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7C,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACpE,CAAC;CACF;AAED,MAAM,MAAM,GAAG,IAAI,iBAAiB,EAAmB,CAAC;AAExD,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,OAAO;QACL,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;QAClC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE;KAC3B,CAAC;AACJ,CAAC","sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface ScopedConnections {\n close(): Promise<void>;\n}\n\n/**\n * Who owns the database connections opened while `run` executes. Outside every scope a\n * handle keeps its own connections for the life of the process.\n */\nexport interface ConnectionScope {\n run<T>(fn: () => T): T;\n close(): Promise<void>;\n}\n\nexport class ConnectionOwner {\n readonly #connections = new Map<object, ScopedConnections>();\n #closed = false;\n\n connectionsFor<C extends ScopedConnections>(key: object, open: () => C): C {\n if (this.#closed) throw new Error(\"This connection scope is closed.\");\n let connections = this.#connections.get(key);\n if (!connections) {\n connections = open();\n this.#connections.set(key, connections);\n }\n return connections as C;\n }\n\n forget(key: object): void {\n this.#connections.delete(key);\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n const open = [...this.#connections.values()];\n this.#connections.clear();\n await Promise.all(open.map((connections) => connections.close()));\n }\n}\n\nconst active = new AsyncLocalStorage<ConnectionOwner>();\n\nexport function currentOwner(): ConnectionOwner | undefined {\n return active.getStore();\n}\n\n/**\n * Connections opened inside `run` belong to this scope and close with it. Use one per\n * runtime context that must not share sockets: a Worker request, or a Durable Object\n * instance, since workerd cancels I/O started in one context and awaited from another.\n */\nexport function createConnectionScope(): ConnectionScope {\n const owner = new ConnectionOwner();\n return {\n run: (fn) => active.run(owner, fn),\n close: () => owner.close(),\n };\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import type { DataProvider, ListenOptions, Unlisten } from "@parlor/core";
2
+ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
3
+ export type Database = PgDatabase<PgQueryResultHKT>;
4
+ declare module "@parlor/core" {
5
+ interface DataRegister {
6
+ database: Database;
7
+ }
8
+ }
9
+ export interface DatabaseOptions {
10
+ readonly url?: string | undefined;
11
+ /**
12
+ * A connection that bypasses any pooler, for LISTEN and migrations. Transaction poolers
13
+ * such as PgBouncer, Neon's pooled endpoint or Hyperdrive cannot hold a LISTEN.
14
+ * Defaults to `DATABASE_DIRECT_URL`, then to `url`.
15
+ */
16
+ readonly directUrl?: string | undefined;
17
+ readonly dataDir?: string | undefined;
18
+ readonly migrationsFolder?: string | undefined;
19
+ }
20
+ /** Runs one parameterized statement (`$1`, `$2`, ...) and answers its rows, as `pg` does. */
21
+ export interface DatabaseClient {
22
+ query(text: string, params?: unknown[]): Promise<{
23
+ rows: unknown[];
24
+ }>;
25
+ }
26
+ export interface DatabaseHandle extends DataProvider<Database> {
27
+ readonly db: Database;
28
+ /**
29
+ * The connection under `db`, for libraries that take a `pg`-style client. Each statement
30
+ * runs on its own, outside any transaction, on the current connection scope's connections.
31
+ */
32
+ readonly client: DatabaseClient;
33
+ readonly driver: "pglite" | "node-postgres";
34
+ listen(channel: string, handler: (payload: string) => void, options?: ListenOptions): Promise<Unlisten>;
35
+ migrate(folder: string): Promise<void>;
36
+ close(): Promise<void>;
37
+ }
38
+ export declare const DEFAULT_PGLITE_DIR = ".parlor/pglite";
39
+ export declare function resolveDatabaseUrl(env?: NodeJS.ProcessEnv): string | undefined;
40
+ /** PGlite's directory when `dataDir` is not given: `pglite` in the run's generated directory. */
41
+ export declare function resolvePgliteDir(env?: NodeJS.ProcessEnv): string;
42
+ export declare function resolveDirectUrl(env?: NodeJS.ProcessEnv): string | undefined;
43
+ export declare function createDatabase(options?: DatabaseOptions): Promise<DatabaseHandle>;
44
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE1E,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAaxE,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;AAEpD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,YAAY;QACpB,QAAQ,EAAE,QAAQ,CAAC;KACpB;CACF;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,cAAe,SAAQ,YAAY,CAAC,QAAQ,CAAC;IAC5D,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;IAC5C,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD,wBAAgB,kBAAkB,CAChC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,SAAS,CAEpB;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG7E;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,SAAS,CAEpB;AA+GD,wBAAsB,cAAc,CAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAazB"}
@@ -0,0 +1,138 @@
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ import pg from "pg";
3
+ import { migratesWhileLoading, pgliteHandle } from "#platform";
4
+ import { ConnectionOwner, currentOwner, } from "./connection-scope.js";
5
+ import { migrateDatabase } from "./migrations.js";
6
+ import { once } from "./once.js";
7
+ export const DEFAULT_PGLITE_DIR = ".parlor/pglite";
8
+ export function resolveDatabaseUrl(env = process.env) {
9
+ return env.DATABASE_URL || undefined;
10
+ }
11
+ /** PGlite's directory when `dataDir` is not given: `pglite` in the run's generated directory. */
12
+ export function resolvePgliteDir(env = process.env) {
13
+ const generated = env.PARLOR_GENERATED_DIR?.replace(/\/+$/, "");
14
+ return generated ? `${generated}/pglite` : DEFAULT_PGLITE_DIR;
15
+ }
16
+ export function resolveDirectUrl(env = process.env) {
17
+ return env.DATABASE_DIRECT_URL || undefined;
18
+ }
19
+ function openPgConnections(url, onClose) {
20
+ const pool = new pg.Pool({ connectionString: url });
21
+ pool.on("error", (error) => {
22
+ console.warn("[parlor] an idle database connection dropped:", error.message);
23
+ });
24
+ const listeners = new Set();
25
+ return {
26
+ pool,
27
+ db: drizzle(pool),
28
+ listeners,
29
+ close: once(async () => {
30
+ onClose?.();
31
+ await Promise.all([...listeners].map((client) => client.end()));
32
+ await pool.end();
33
+ }),
34
+ };
35
+ }
36
+ /** Stays valid across scopes, so code that captured `handle.db` at startup still works. */
37
+ function scopedDatabase(current) {
38
+ return new Proxy({}, {
39
+ get(_target, property) {
40
+ const db = current();
41
+ const value = Reflect.get(db, property, db);
42
+ return typeof value === "function" ? value.bind(db) : value;
43
+ },
44
+ has: (_target, property) => Reflect.has(current(), property),
45
+ getPrototypeOf: () => Reflect.getPrototypeOf(current()),
46
+ });
47
+ }
48
+ function nodePostgresHandle(url, directUrl) {
49
+ const key = {};
50
+ const outside = new ConnectionOwner();
51
+ const opened = new Set();
52
+ const connections = () => (currentOwner() ?? outside).connectionsFor(key, () => {
53
+ const created = openPgConnections(url, () => opened.delete(created));
54
+ opened.add(created);
55
+ return created;
56
+ });
57
+ return {
58
+ db: scopedDatabase(() => connections().db),
59
+ client: { query: (text, params) => connections().pool.query(text, params) },
60
+ driver: "node-postgres",
61
+ read: () => connections().db,
62
+ transaction: (run) => connections().db.transaction((tx) => run(tx)),
63
+ listen: async (channel, handler, options = {}) => {
64
+ const { listeners } = connections();
65
+ const client = new pg.Client({ connectionString: directUrl });
66
+ let state = "listening";
67
+ const current = () => state;
68
+ const lose = () => {
69
+ if (state !== "listening")
70
+ return;
71
+ state = "lost";
72
+ listeners.delete(client);
73
+ client.end().catch(() => { });
74
+ options.onLost?.();
75
+ };
76
+ client.on("error", lose);
77
+ client.on("end", lose);
78
+ client.on("notification", (message) => {
79
+ if (message.channel === channel)
80
+ handler(message.payload ?? "");
81
+ });
82
+ const identifier = pg.escapeIdentifier(channel);
83
+ try {
84
+ await client.connect();
85
+ await client.query(`LISTEN ${identifier}`);
86
+ }
87
+ catch (error) {
88
+ state = "closed";
89
+ await client.end().catch(() => { });
90
+ throw error;
91
+ }
92
+ if (current() === "lost")
93
+ throw new Error(`LISTEN ${identifier} lost its connection`);
94
+ listeners.add(client);
95
+ return async () => {
96
+ if (state !== "listening")
97
+ return;
98
+ state = "closed";
99
+ listeners.delete(client);
100
+ await client.query(`UNLISTEN ${identifier}`).catch(() => { });
101
+ await client.end();
102
+ };
103
+ },
104
+ migrate: async (folder) => {
105
+ if (directUrl === url) {
106
+ await migrateDatabase(connections().db, folder);
107
+ return;
108
+ }
109
+ const direct = openPgConnections(directUrl);
110
+ try {
111
+ await migrateDatabase(direct.db, folder);
112
+ }
113
+ finally {
114
+ await direct.close();
115
+ }
116
+ },
117
+ close: once(async () => {
118
+ outside.forget(key);
119
+ await Promise.all([...opened].map((created) => created.close()));
120
+ }),
121
+ };
122
+ }
123
+ export async function createDatabase(options = {}) {
124
+ const url = options.url ?? resolveDatabaseUrl();
125
+ const handle = url
126
+ ? nodePostgresHandle(url, options.directUrl ?? resolveDirectUrl() ?? url)
127
+ : await pgliteHandle(options.dataDir ?? resolvePgliteDir());
128
+ const folder = options.migrationsFolder;
129
+ if (!folder)
130
+ return handle;
131
+ if (migratesWhileLoading) {
132
+ await handle.migrate(folder);
133
+ return handle;
134
+ }
135
+ let migrated;
136
+ return { ...handle, prepare: () => (migrated ??= handle.migrate(folder)) };
137
+ }
138
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DataProvider, ListenOptions, Unlisten } from \"@parlor/core\";\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { PgDatabase, PgQueryResultHKT } from \"drizzle-orm/pg-core\";\nimport pg from \"pg\";\n\nimport { migratesWhileLoading, pgliteHandle } from \"#platform\";\n\nimport {\n ConnectionOwner,\n currentOwner,\n type ScopedConnections,\n} from \"./connection-scope.js\";\nimport { migrateDatabase } from \"./migrations.js\";\nimport { once } from \"./once.js\";\n\nexport type Database = PgDatabase<PgQueryResultHKT>;\n\ndeclare module \"@parlor/core\" {\n interface DataRegister {\n database: Database;\n }\n}\n\nexport interface DatabaseOptions {\n readonly url?: string | undefined;\n /**\n * A connection that bypasses any pooler, for LISTEN and migrations. Transaction poolers\n * such as PgBouncer, Neon's pooled endpoint or Hyperdrive cannot hold a LISTEN.\n * Defaults to `DATABASE_DIRECT_URL`, then to `url`.\n */\n readonly directUrl?: string | undefined;\n readonly dataDir?: string | undefined;\n readonly migrationsFolder?: string | undefined;\n}\n\n/** Runs one parameterized statement (`$1`, `$2`, ...) and answers its rows, as `pg` does. */\nexport interface DatabaseClient {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\nexport interface DatabaseHandle extends DataProvider<Database> {\n readonly db: Database;\n /**\n * The connection under `db`, for libraries that take a `pg`-style client. Each statement\n * runs on its own, outside any transaction, on the current connection scope's connections.\n */\n readonly client: DatabaseClient;\n readonly driver: \"pglite\" | \"node-postgres\";\n listen(\n channel: string,\n handler: (payload: string) => void,\n options?: ListenOptions,\n ): Promise<Unlisten>;\n migrate(folder: string): Promise<void>;\n close(): Promise<void>;\n}\n\nexport const DEFAULT_PGLITE_DIR = \".parlor/pglite\";\n\nexport function resolveDatabaseUrl(\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n return env.DATABASE_URL || undefined;\n}\n\n/** PGlite's directory when `dataDir` is not given: `pglite` in the run's generated directory. */\nexport function resolvePgliteDir(env: NodeJS.ProcessEnv = process.env): string {\n const generated = env.PARLOR_GENERATED_DIR?.replace(/\\/+$/, \"\");\n return generated ? `${generated}/pglite` : DEFAULT_PGLITE_DIR;\n}\n\nexport function resolveDirectUrl(\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n return env.DATABASE_DIRECT_URL || undefined;\n}\n\ninterface PgConnections extends ScopedConnections {\n readonly pool: pg.Pool;\n readonly db: NodePgDatabase;\n readonly listeners: Set<pg.Client>;\n}\n\nfunction openPgConnections(url: string, onClose?: () => void): PgConnections {\n const pool = new pg.Pool({ connectionString: url });\n pool.on(\"error\", (error) => {\n console.warn(\"[parlor] an idle database connection dropped:\", error.message);\n });\n const listeners = new Set<pg.Client>();\n return {\n pool,\n db: drizzle(pool),\n listeners,\n close: once(async () => {\n onClose?.();\n await Promise.all([...listeners].map((client) => client.end()));\n await pool.end();\n }),\n };\n}\n\n/** Stays valid across scopes, so code that captured `handle.db` at startup still works. */\nfunction scopedDatabase(current: () => Database): Database {\n return new Proxy({} as Database, {\n get(_target, property) {\n const db = current();\n const value: unknown = Reflect.get(db, property, db);\n return typeof value === \"function\" ? value.bind(db) : value;\n },\n has: (_target, property) => Reflect.has(current(), property),\n getPrototypeOf: () => Reflect.getPrototypeOf(current()),\n });\n}\n\nfunction nodePostgresHandle(url: string, directUrl: string): DatabaseHandle {\n const key = {};\n const outside = new ConnectionOwner();\n const opened = new Set<PgConnections>();\n const connections = () =>\n (currentOwner() ?? outside).connectionsFor(key, () => {\n const created = openPgConnections(url, () => opened.delete(created));\n opened.add(created);\n return created;\n });\n return {\n db: scopedDatabase(() => connections().db),\n client: { query: (text, params) => connections().pool.query(text, params) },\n driver: \"node-postgres\",\n read: () => connections().db,\n transaction: (run) => connections().db.transaction((tx) => run(tx)),\n listen: async (channel, handler, options = {}) => {\n const { listeners } = connections();\n const client = new pg.Client({ connectionString: directUrl });\n let state: \"listening\" | \"lost\" | \"closed\" = \"listening\";\n const current = () => state;\n const lose = () => {\n if (state !== \"listening\") return;\n state = \"lost\";\n listeners.delete(client);\n client.end().catch(() => {});\n options.onLost?.();\n };\n client.on(\"error\", lose);\n client.on(\"end\", lose);\n client.on(\"notification\", (message) => {\n if (message.channel === channel) handler(message.payload ?? \"\");\n });\n const identifier = pg.escapeIdentifier(channel);\n try {\n await client.connect();\n await client.query(`LISTEN ${identifier}`);\n } catch (error) {\n state = \"closed\";\n await client.end().catch(() => {});\n throw error;\n }\n if (current() === \"lost\")\n throw new Error(`LISTEN ${identifier} lost its connection`);\n listeners.add(client);\n return async () => {\n if (state !== \"listening\") return;\n state = \"closed\";\n listeners.delete(client);\n await client.query(`UNLISTEN ${identifier}`).catch(() => {});\n await client.end();\n };\n },\n migrate: async (folder) => {\n if (directUrl === url) {\n await migrateDatabase(connections().db, folder);\n return;\n }\n const direct = openPgConnections(directUrl);\n try {\n await migrateDatabase(direct.db, folder);\n } finally {\n await direct.close();\n }\n },\n close: once(async () => {\n outside.forget(key);\n await Promise.all([...opened].map((created) => created.close()));\n }),\n };\n}\n\nexport async function createDatabase(\n options: DatabaseOptions = {},\n): Promise<DatabaseHandle> {\n const url = options.url ?? resolveDatabaseUrl();\n const handle = url\n ? nodePostgresHandle(url, options.directUrl ?? resolveDirectUrl() ?? url)\n : await pgliteHandle(options.dataDir ?? resolvePgliteDir());\n const folder = options.migrationsFolder;\n if (!folder) return handle;\n if (migratesWhileLoading) {\n await handle.migrate(folder);\n return handle;\n }\n let migrated: Promise<void> | undefined;\n return { ...handle, prepare: () => (migrated ??= handle.migrate(folder)) };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export { type ConnectionScope, createConnectionScope } from "./connection-scope.js";
2
+ export { createDatabase, type Database, type DatabaseClient, type DatabaseHandle, type DatabaseOptions, DEFAULT_PGLITE_DIR, resolveDatabaseUrl, resolveDirectUrl, resolvePgliteDir, } from "./database.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EACL,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createConnectionScope } from "./connection-scope.js";
2
+ export { createDatabase, DEFAULT_PGLITE_DIR, resolveDatabaseUrl, resolveDirectUrl, resolvePgliteDir, } from "./database.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EACL,cAAc,EAKd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,eAAe,CAAC","sourcesContent":["export { type ConnectionScope, createConnectionScope } from \"./connection-scope.js\";\nexport {\n createDatabase,\n type Database,\n type DatabaseClient,\n type DatabaseHandle,\n type DatabaseOptions,\n DEFAULT_PGLITE_DIR,\n resolveDatabaseUrl,\n resolveDirectUrl,\n resolvePgliteDir,\n} from \"./database.js\";\n"]}
@@ -0,0 +1,6 @@
1
+ import { type MigrationMeta } from "drizzle-orm/migrator";
2
+ import type { Database } from "./database.js";
3
+ export declare function migrationsIn(folder: string): MigrationMeta[];
4
+ /** Drizzle's own migrator, fed from the build when there is no filesystem to read. */
5
+ export declare function migrateDatabase(db: Database, folder: string): Promise<void>;
6
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAW9C,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAU5D;AAOD,sFAAsF;AACtF,wBAAsB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjF"}
@@ -0,0 +1,21 @@
1
+ import { readMigrationFiles } from "drizzle-orm/migrator";
2
+ /** Builds without a filesystem, such as Cloudflare's, embed the migrations keyed by folder. */
3
+ function embedded() {
4
+ return typeof __PARLOR_MIGRATIONS__ === "undefined" ? undefined : __PARLOR_MIGRATIONS__;
5
+ }
6
+ export function migrationsIn(folder) {
7
+ const bundled = embedded();
8
+ if (!bundled)
9
+ return readMigrationFiles({ migrationsFolder: folder });
10
+ const migrations = bundled[folder];
11
+ if (!migrations) {
12
+ throw new Error(`The migrations in "${folder}" are not in this build. Parlor embeds the folder set as database.migrations in parlor.config.ts.`);
13
+ }
14
+ return migrations;
15
+ }
16
+ /** Drizzle's own migrator, fed from the build when there is no filesystem to read. */
17
+ export async function migrateDatabase(db, folder) {
18
+ const { dialect, session } = db;
19
+ await dialect.migrate(migrationsIn(folder), session, { migrationsFolder: folder });
20
+ }
21
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAS9E,+FAA+F;AAC/F,SAAS,QAAQ;IACf,OAAO,OAAO,qBAAqB,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAAC;IACtE,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,sBAAsB,MAAM,mGAAmG,CAChI,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAOD,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EAAY,EAAE,MAAc;IAChE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAA2B,CAAC;IACzD,MAAM,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAAC;AACrF,CAAC","sourcesContent":["import { type MigrationMeta, readMigrationFiles } from \"drizzle-orm/migrator\";\nimport type { PgDialect, PgSession } from \"drizzle-orm/pg-core\";\n\nimport type { Database } from \"./database.js\";\n\ndeclare const __PARLOR_MIGRATIONS__:\n | Readonly<Record<string, MigrationMeta[]>>\n | undefined;\n\n/** Builds without a filesystem, such as Cloudflare's, embed the migrations keyed by folder. */\nfunction embedded(): Readonly<Record<string, MigrationMeta[]>> | undefined {\n return typeof __PARLOR_MIGRATIONS__ === \"undefined\" ? undefined : __PARLOR_MIGRATIONS__;\n}\n\nexport function migrationsIn(folder: string): MigrationMeta[] {\n const bundled = embedded();\n if (!bundled) return readMigrationFiles({ migrationsFolder: folder });\n const migrations = bundled[folder];\n if (!migrations) {\n throw new Error(\n `The migrations in \"${folder}\" are not in this build. Parlor embeds the folder set as database.migrations in parlor.config.ts.`,\n );\n }\n return migrations;\n}\n\ninterface Migratable {\n readonly dialect: PgDialect;\n readonly session: PgSession;\n}\n\n/** Drizzle's own migrator, fed from the build when there is no filesystem to read. */\nexport async function migrateDatabase(db: Database, folder: string): Promise<void> {\n const { dialect, session } = db as unknown as Migratable;\n await dialect.migrate(migrationsIn(folder), session, { migrationsFolder: folder });\n}\n"]}
package/dist/once.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function once(run: () => Promise<void>): () => Promise<void>;
2
+ //# sourceMappingURL=once.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"once.d.ts","sourceRoot":"","sources":["../src/once.ts"],"names":[],"mappings":"AAAA,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAGlE"}
package/dist/once.js ADDED
@@ -0,0 +1,5 @@
1
+ export function once(run) {
2
+ let result;
3
+ return () => (result ??= run());
4
+ }
5
+ //# sourceMappingURL=once.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"once.js","sourceRoot":"","sources":["../src/once.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,IAAI,CAAC,GAAwB;IAC3C,IAAI,MAAiC,CAAC;IACtC,OAAO,GAAG,EAAE,CAAC,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["export function once(run: () => Promise<void>): () => Promise<void> {\n let result: Promise<void> | undefined;\n return () => (result ??= run());\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { DatabaseHandle } from "./database.js";
2
+ export declare function pgliteHandle(dataDir: string): Promise<DatabaseHandle>;
3
+ //# sourceMappingURL=pglite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../src/pglite.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AASpD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAiB3E"}
package/dist/pglite.js ADDED
@@ -0,0 +1,30 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { PGlite } from "@electric-sql/pglite";
4
+ import { drizzle } from "drizzle-orm/pglite";
5
+ import { migrateDatabase } from "./migrations.js";
6
+ import { once } from "./once.js";
7
+ async function prepareDataDir(dataDir) {
8
+ if (/^[a-z]+:\/\//i.test(dataDir))
9
+ return;
10
+ await mkdir(dirname(dataDir), { recursive: true });
11
+ }
12
+ export async function pgliteHandle(dataDir) {
13
+ await prepareDataDir(dataDir);
14
+ const client = new PGlite(dataDir);
15
+ const db = drizzle(client);
16
+ return {
17
+ db,
18
+ client: { query: (text, params) => client.query(text, params) },
19
+ driver: "pglite",
20
+ read: () => db,
21
+ transaction: (run) => db.transaction((tx) => run(tx)),
22
+ listen: async (channel, handler) => {
23
+ const unlisten = await client.listen(channel, handler);
24
+ return () => unlisten();
25
+ },
26
+ migrate: (folder) => migrateDatabase(db, folder),
27
+ close: once(() => client.close()),
28
+ };
29
+ }
30
+ //# sourceMappingURL=pglite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pglite.js","sourceRoot":"","sources":["../src/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,KAAK,UAAU,cAAc,CAAC,OAAe;IAC3C,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO;IAC1C,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,OAAO;QACL,EAAE;QACF,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE;QAC/D,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE;QACd,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;YACjC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACvD,OAAO,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;QAC1B,CAAC;QACD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC;QAChD,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC","sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { PGlite } from \"@electric-sql/pglite\";\nimport { drizzle } from \"drizzle-orm/pglite\";\n\nimport type { DatabaseHandle } from \"./database.js\";\nimport { migrateDatabase } from \"./migrations.js\";\nimport { once } from \"./once.js\";\n\nasync function prepareDataDir(dataDir: string): Promise<void> {\n if (/^[a-z]+:\\/\\//i.test(dataDir)) return;\n await mkdir(dirname(dataDir), { recursive: true });\n}\n\nexport async function pgliteHandle(dataDir: string): Promise<DatabaseHandle> {\n await prepareDataDir(dataDir);\n const client = new PGlite(dataDir);\n const db = drizzle(client);\n return {\n db,\n client: { query: (text, params) => client.query(text, params) },\n driver: \"pglite\",\n read: () => db,\n transaction: (run) => db.transaction((tx) => run(tx)),\n listen: async (channel, handler) => {\n const unlisten = await client.listen(channel, handler);\n return () => unlisten();\n },\n migrate: (folder) => migrateDatabase(db, folder),\n close: once(() => client.close()),\n };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare const migratesWhileLoading = true;
2
+ export { pgliteHandle } from "./pglite.js";
3
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ export const migratesWhileLoading = true;
2
+ export { pgliteHandle } from "./pglite.js";
3
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export const migratesWhileLoading = true;\n\nexport { pgliteHandle } from \"./pglite.js\";\n"]}
@@ -0,0 +1,5 @@
1
+ import type { DatabaseHandle } from "./database.js";
2
+ /** workerd allows no I/O while the app's modules load, so migrations wait for `prepare`. */
3
+ export declare const migratesWhileLoading = false;
4
+ export declare function pgliteHandle(): Promise<DatabaseHandle>;
5
+ //# sourceMappingURL=platform.workerd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.workerd.d.ts","sourceRoot":"","sources":["../src/platform.workerd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,QAAQ,CAAC;AAE1C,wBAAgB,YAAY,IAAI,OAAO,CAAC,cAAc,CAAC,CAMtD"}
@@ -0,0 +1,6 @@
1
+ /** workerd allows no I/O while the app's modules load, so migrations wait for `prepare`. */
2
+ export const migratesWhileLoading = false;
3
+ export function pgliteHandle() {
4
+ return Promise.reject(new Error("DATABASE_URL is not set. PGlite only runs in development; on Cloudflare, set DATABASE_URL to a Postgres connection string."));
5
+ }
6
+ //# sourceMappingURL=platform.workerd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.workerd.js","sourceRoot":"","sources":["../src/platform.workerd.ts"],"names":[],"mappings":"AAEA,4FAA4F;AAC5F,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAE1C,MAAM,UAAU,YAAY;IAC1B,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CACP,4HAA4H,CAC7H,CACF,CAAC;AACJ,CAAC","sourcesContent":["import type { DatabaseHandle } from \"./database.js\";\n\n/** workerd allows no I/O while the app's modules load, so migrations wait for `prepare`. */\nexport const migratesWhileLoading = false;\n\nexport function pgliteHandle(): Promise<DatabaseHandle> {\n return Promise.reject(\n new Error(\n \"DATABASE_URL is not set. PGlite only runs in development; on Cloudflare, set DATABASE_URL to a Postgres connection string.\",\n ),\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@parlor/data",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "homepage": "https://cychien.github.io/parlor",
5
+ "bugs": "https://github.com/cychien/parlor/issues",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cychien/parlor.git",
10
+ "directory": "packages/data"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "imports": {
17
+ "#platform": {
18
+ "workerd": "./dist/platform.workerd.js",
19
+ "default": "./dist/platform.js"
20
+ }
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "@electric-sql/pglite": "^0.5.9",
30
+ "drizzle-orm": "^0.45.4",
31
+ "pg": "^8.23.1",
32
+ "@parlor/core": "0.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^24.14.0",
36
+ "@types/pg": "^8.23.1",
37
+ "typescript": "^7.0.2",
38
+ "vitest": "^5.0.3"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run"
44
+ }
6
45
  }