@unieojs/unio-supabase-shim 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.
@@ -0,0 +1,139 @@
1
+ export class SupabasePostgrestBuilder {
2
+ #database;
3
+ #table;
4
+ #state;
5
+ constructor(database, table, state) {
6
+ this.#database = database;
7
+ this.#table = table;
8
+ this.#state = {
9
+ action: state?.action ?? "select",
10
+ columns: state?.columns,
11
+ filters: state?.filters ?? [],
12
+ order: state?.order,
13
+ rows: state?.rows,
14
+ wantsRows: state?.wantsRows ?? true,
15
+ single: state?.single ?? false,
16
+ };
17
+ }
18
+ select(columns = "*") {
19
+ this.#state.columns = parseColumns(columns);
20
+ this.#state.wantsRows = true;
21
+ return this;
22
+ }
23
+ insert(rows) {
24
+ this.#state.action = "insert";
25
+ this.#state.rows = Array.isArray(rows) ? rows : [rows];
26
+ this.#state.wantsRows = false;
27
+ return this;
28
+ }
29
+ delete() {
30
+ this.#state.action = "delete";
31
+ this.#state.wantsRows = false;
32
+ return this;
33
+ }
34
+ eq(column, value) {
35
+ this.#state.filters.push({ column, operator: "eq", value });
36
+ return this;
37
+ }
38
+ gte(column, value) {
39
+ this.#state.filters.push({ column, operator: "gte", value });
40
+ return this;
41
+ }
42
+ lte(column, value) {
43
+ this.#state.filters.push({ column, operator: "lte", value });
44
+ return this;
45
+ }
46
+ order(column, options = {}) {
47
+ this.#state.order = { column, ascending: options.ascending ?? true };
48
+ return this;
49
+ }
50
+ single() {
51
+ this.#state.single = true;
52
+ return this;
53
+ }
54
+ then(onfulfilled, onrejected) {
55
+ return this.#execute().then(onfulfilled, onrejected);
56
+ }
57
+ catch(onrejected) {
58
+ return this.#execute().catch(onrejected);
59
+ }
60
+ finally(onfinally) {
61
+ return this.#execute().finally(onfinally ?? undefined);
62
+ }
63
+ async #execute() {
64
+ try {
65
+ const rows = await this.#executeRows();
66
+ if (this.#state.single) {
67
+ return singleResponse(rows);
68
+ }
69
+ return {
70
+ data: rows,
71
+ error: null,
72
+ count: rows.length,
73
+ status: this.#state.action === "insert" ? 201 : 200,
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ data: (this.#state.single ? null : []),
79
+ error: {
80
+ message: error instanceof Error ? error.message : String(error),
81
+ status: 500,
82
+ },
83
+ count: null,
84
+ status: 500,
85
+ };
86
+ }
87
+ }
88
+ async #executeRows() {
89
+ if (this.#state.action === "insert") {
90
+ if (!this.#state.wantsRows) {
91
+ await this.#database.insert(this.#table, this.#state.rows ?? []);
92
+ return [];
93
+ }
94
+ return this.#database.insert(this.#table, this.#state.rows ?? [], {
95
+ columns: this.#state.columns,
96
+ filters: this.#state.filters,
97
+ order: this.#state.order,
98
+ });
99
+ }
100
+ if (this.#state.action === "delete") {
101
+ if (!this.#state.wantsRows) {
102
+ await this.#database.delete(this.#table, { filters: this.#state.filters });
103
+ return [];
104
+ }
105
+ return this.#database.delete(this.#table, {
106
+ columns: this.#state.columns,
107
+ filters: this.#state.filters,
108
+ order: this.#state.order,
109
+ });
110
+ }
111
+ return this.#database.select(this.#table, {
112
+ columns: this.#state.columns,
113
+ filters: this.#state.filters,
114
+ order: this.#state.order,
115
+ });
116
+ }
117
+ }
118
+ function singleResponse(rows) {
119
+ if (rows.length === 1) {
120
+ return { data: rows[0], error: null, count: 1, status: 200 };
121
+ }
122
+ return {
123
+ data: null,
124
+ error: {
125
+ code: "PGRST116",
126
+ message: "JSON object requested, multiple (or no) rows returned",
127
+ status: 406,
128
+ },
129
+ count: rows.length,
130
+ status: 406,
131
+ };
132
+ }
133
+ function parseColumns(columns) {
134
+ return columns
135
+ .split(",")
136
+ .map((column) => column.trim())
137
+ .filter(Boolean);
138
+ }
139
+ //# sourceMappingURL=query-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-builder.js","sourceRoot":"","sources":["../src/query-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { SnapshotStorage } from "./types.js";
2
+ export declare class InMemorySnapshotStorage implements SnapshotStorage {
3
+ #private;
4
+ get(key: string): string | null;
5
+ set(key: string, snapshot: string): void;
6
+ }
@@ -0,0 +1,10 @@
1
+ export class InMemorySnapshotStorage {
2
+ #snapshots = new Map();
3
+ get(key) {
4
+ return this.#snapshots.get(key) ?? null;
5
+ }
6
+ set(key, snapshot) {
7
+ this.#snapshots.set(key, snapshot);
8
+ }
9
+ }
10
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,uBAAuB;IACzB,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEhD,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAC1C,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,QAAgB;QAC/B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;CACF"}
@@ -0,0 +1,54 @@
1
+ export type ShimValue = string | number | boolean | null;
2
+ export type ShimRow = Record<string, ShimValue>;
3
+ export type AuthChangeEvent = "SIGNED_UP" | "SIGNED_IN" | "SIGNED_OUT";
4
+ export interface ShimUser {
5
+ id: string;
6
+ email: string;
7
+ }
8
+ export interface ShimSession {
9
+ access_token: string;
10
+ token_type: "bearer";
11
+ user: ShimUser;
12
+ }
13
+ export interface SupabaseShimError {
14
+ code?: string;
15
+ message: string;
16
+ status?: number;
17
+ }
18
+ export interface SupabaseAuthResponse<T> {
19
+ data: T;
20
+ error: SupabaseShimError | null;
21
+ }
22
+ export interface PostgrestResponse<T> {
23
+ data: T;
24
+ error: SupabaseShimError | null;
25
+ count: number | null;
26
+ status: number;
27
+ }
28
+ export interface QueryFilter {
29
+ column: string;
30
+ operator: "eq" | "gte" | "lte";
31
+ value: ShimValue;
32
+ }
33
+ export interface QueryOrder {
34
+ column: string;
35
+ ascending: boolean;
36
+ }
37
+ export interface SelectQuery {
38
+ columns?: string[];
39
+ filters?: QueryFilter[];
40
+ order?: QueryOrder;
41
+ }
42
+ export interface SupabaseShimDatabase {
43
+ bootstrap(initSql: string): Promise<void>;
44
+ select(table: string, query?: SelectQuery): Promise<ShimRow[]>;
45
+ insert(table: string, rows: ShimRow[], query?: SelectQuery): Promise<ShimRow[]>;
46
+ delete(table: string, query?: SelectQuery): Promise<ShimRow[]>;
47
+ snapshot(): Promise<string>;
48
+ restore(snapshot: string): Promise<void>;
49
+ close?(): Promise<void>;
50
+ }
51
+ export interface SnapshotStorage {
52
+ get(key: string): Promise<string | null> | string | null;
53
+ set(key: string, snapshot: string): Promise<void> | void;
54
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@unieojs/unio-supabase-shim",
3
+ "version": "0.1.0",
4
+ "description": "Minimal Supabase-compatible shim helpers for Unio generated apps backed by PGlite.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/unieojs/unio-adapters.git",
18
+ "directory": "packages/supabase-shim"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "test": "node --import tsx --test \"test/**/*.test.ts\"",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "@electric-sql/pglite": "^0.5.3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.15.0",
38
+ "tsx": "^4.20.0",
39
+ "typescript": "^5.8.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.19.0"
43
+ },
44
+ "license": "MIT"
45
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type {
2
+ AuthChangeEvent,
3
+ ShimSession,
4
+ ShimUser,
5
+ SupabaseAuthResponse,
6
+ } from "./types.js";
7
+
8
+ interface StoredUser extends ShimUser {
9
+ password: string;
10
+ }
11
+
12
+ type AuthListener = (event: AuthChangeEvent, session: ShimSession | null) => void;
13
+
14
+ export class SupabaseShimAuthClient {
15
+ readonly #usersByEmail = new Map<string, StoredUser>();
16
+ readonly #listeners = new Set<AuthListener>();
17
+ #session: ShimSession | null = null;
18
+ #nextUserId = 1;
19
+
20
+ async signUp(credentials: {
21
+ email: string;
22
+ password: string;
23
+ }): Promise<SupabaseAuthResponse<{ user: ShimUser; session: ShimSession }>> {
24
+ if (this.#usersByEmail.has(credentials.email)) {
25
+ return {
26
+ data: { user: { id: "", email: credentials.email }, session: emptySession() },
27
+ error: { message: "User already registered", status: 400 },
28
+ };
29
+ }
30
+
31
+ const user: StoredUser = {
32
+ id: `shim-user-${this.#nextUserId.toString(36)}`,
33
+ email: credentials.email,
34
+ password: credentials.password,
35
+ };
36
+ this.#nextUserId += 1;
37
+ this.#usersByEmail.set(user.email, user);
38
+ const session = createSession(user);
39
+ this.#session = session;
40
+ this.#emit("SIGNED_UP", session);
41
+
42
+ return { data: { user: publicUser(user), session }, error: null };
43
+ }
44
+
45
+ async signInWithPassword(credentials: {
46
+ email: string;
47
+ password: string;
48
+ }): Promise<SupabaseAuthResponse<{ user: ShimUser | null; session: ShimSession | null }>> {
49
+ const user = this.#usersByEmail.get(credentials.email);
50
+ if (!user || user.password !== credentials.password) {
51
+ return {
52
+ data: { user: null, session: null },
53
+ error: { message: "Invalid login credentials", status: 400 },
54
+ };
55
+ }
56
+
57
+ this.#session = createSession(user);
58
+ this.#emit("SIGNED_IN", this.#session);
59
+ return {
60
+ data: { user: this.#session.user, session: this.#session },
61
+ error: null,
62
+ };
63
+ }
64
+
65
+ async getSession(): Promise<SupabaseAuthResponse<{ session: ShimSession | null }>> {
66
+ return { data: { session: this.#session }, error: null };
67
+ }
68
+
69
+ onAuthStateChange(listener: AuthListener): {
70
+ data: { subscription: { unsubscribe: () => void } };
71
+ } {
72
+ this.#listeners.add(listener);
73
+ return {
74
+ data: {
75
+ subscription: {
76
+ unsubscribe: () => {
77
+ this.#listeners.delete(listener);
78
+ },
79
+ },
80
+ },
81
+ };
82
+ }
83
+
84
+ #emit(event: AuthChangeEvent, session: ShimSession | null): void {
85
+ for (const listener of this.#listeners) {
86
+ listener(event, session);
87
+ }
88
+ }
89
+ }
90
+
91
+ function createSession(user: ShimUser): ShimSession {
92
+ return {
93
+ access_token: `shim-token-${user.id}`,
94
+ token_type: "bearer",
95
+ user: publicUser(user),
96
+ };
97
+ }
98
+
99
+ function publicUser(user: ShimUser): ShimUser {
100
+ return { id: user.id, email: user.email };
101
+ }
102
+
103
+ function emptySession(): ShimSession {
104
+ return {
105
+ access_token: "",
106
+ token_type: "bearer",
107
+ user: { id: "", email: "" },
108
+ };
109
+ }
package/src/client.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { SupabaseShimAuthClient } from "./auth.js";
2
+ import { SupabasePostgrestBuilder } from "./query-builder.js";
3
+ import type { SupabaseShimDatabase } from "./types.js";
4
+
5
+ export class SupabaseShimClient {
6
+ readonly auth = new SupabaseShimAuthClient();
7
+ readonly #database: SupabaseShimDatabase;
8
+
9
+ constructor(database: SupabaseShimDatabase) {
10
+ this.#database = database;
11
+ }
12
+
13
+ from(table: string): SupabasePostgrestBuilder {
14
+ return new SupabasePostgrestBuilder(this.#database, table);
15
+ }
16
+ }
package/src/handler.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { SupabasePostgrestBuilder } from "./query-builder.js";
2
+ import type { SupabaseShimClient } from "./client.js";
3
+ import type { ShimValue } from "./types.js";
4
+
5
+ export async function handleSupabaseShimRequest(
6
+ request: Request,
7
+ client: SupabaseShimClient,
8
+ basePath = "/api/_supabase",
9
+ ): Promise<Response> {
10
+ const url = new URL(request.url);
11
+ if (!url.pathname.startsWith(basePath)) {
12
+ return jsonResponse({ error: "Not found" }, 404);
13
+ }
14
+
15
+ const path = url.pathname.slice(basePath.length).replace(/^\/+/, "");
16
+ if (path.startsWith("auth/v1/")) {
17
+ return handleAuthRequest(path, request, client);
18
+ }
19
+ if (path.startsWith("rest/v1/")) {
20
+ return handleRestRequest(path, request, url, client);
21
+ }
22
+
23
+ return jsonResponse({ error: "Not found" }, 404);
24
+ }
25
+
26
+ async function handleAuthRequest(
27
+ path: string,
28
+ request: Request,
29
+ client: SupabaseShimClient,
30
+ ): Promise<Response> {
31
+ if (request.method === "POST" && path === "auth/v1/signup") {
32
+ const body = (await request.json()) as { email?: string; password?: string };
33
+ const result = await client.auth.signUp({
34
+ email: body.email ?? "",
35
+ password: body.password ?? "",
36
+ });
37
+ return jsonResponse(result.error ? { error: result.error } : result.data, result.error?.status ?? 200);
38
+ }
39
+
40
+ if (request.method === "POST" && path === "auth/v1/token") {
41
+ const body = (await request.json()) as { email?: string; password?: string };
42
+ const result = await client.auth.signInWithPassword({
43
+ email: body.email ?? "",
44
+ password: body.password ?? "",
45
+ });
46
+ return jsonResponse(result.error ? { error: result.error } : result.data, result.error?.status ?? 200);
47
+ }
48
+
49
+ if (request.method === "GET" && path === "auth/v1/session") {
50
+ const result = await client.auth.getSession();
51
+ return jsonResponse(result.data, 200);
52
+ }
53
+
54
+ return jsonResponse({ error: "Unsupported auth endpoint" }, 404);
55
+ }
56
+
57
+ async function handleRestRequest(
58
+ path: string,
59
+ request: Request,
60
+ url: URL,
61
+ client: SupabaseShimClient,
62
+ ): Promise<Response> {
63
+ const table = decodeURIComponent(path.slice("rest/v1/".length).split("/")[0] ?? "");
64
+ if (!table) {
65
+ return jsonResponse({ error: "Missing table" }, 400);
66
+ }
67
+
68
+ if (request.method === "GET") {
69
+ const builder = applyUrlQuery(client.from(table).select(url.searchParams.get("select") ?? "*"), url);
70
+ if (wantsSingleObject(request)) {
71
+ return jsonResponseFromPostgrest(await builder.single());
72
+ }
73
+ return jsonResponseFromPostgrest(await builder);
74
+ }
75
+
76
+ if (request.method === "POST") {
77
+ const body = await request.json();
78
+ const rows = Array.isArray(body) ? body : [body];
79
+ const builder = client.from(table).insert(rows);
80
+ if (url.searchParams.has("select")) {
81
+ builder.select(url.searchParams.get("select") ?? "*");
82
+ }
83
+ return jsonResponseFromPostgrest(await applyUrlQuery(builder, url));
84
+ }
85
+
86
+ if (request.method === "DELETE") {
87
+ const builder = applyUrlQuery(client.from(table).delete(), url);
88
+ if (url.searchParams.has("select")) {
89
+ builder.select(url.searchParams.get("select") ?? "*");
90
+ }
91
+ return jsonResponseFromPostgrest(await builder);
92
+ }
93
+
94
+ return jsonResponse({ error: "Unsupported PostgREST method" }, 405);
95
+ }
96
+
97
+ function applyUrlQuery(builder: SupabasePostgrestBuilder, url: URL): SupabasePostgrestBuilder {
98
+ for (const [key, value] of url.searchParams.entries()) {
99
+ if (key === "select") {
100
+ continue;
101
+ }
102
+ if (key === "order") {
103
+ const [column, direction] = value.split(".");
104
+ if (column) {
105
+ builder.order(column, { ascending: direction !== "desc" });
106
+ }
107
+ continue;
108
+ }
109
+
110
+ const [operator, rawValue] = value.split(".", 2);
111
+ if (operator === "eq" || operator === "gte" || operator === "lte") {
112
+ builder[operator](key, parseUrlValue(rawValue ?? ""));
113
+ }
114
+ }
115
+ return builder;
116
+ }
117
+
118
+ function wantsSingleObject(request: Request): boolean {
119
+ return (request.headers.get("accept") ?? "")
120
+ .split(",")
121
+ .map((value) => value.trim().toLowerCase())
122
+ .includes("application/vnd.pgrst.object+json");
123
+ }
124
+
125
+ function parseUrlValue(value: string): ShimValue {
126
+ if (value === "null") {
127
+ return null;
128
+ }
129
+ if (value === "true") {
130
+ return true;
131
+ }
132
+ if (value === "false") {
133
+ return false;
134
+ }
135
+ const numeric = Number(value);
136
+ return Number.isFinite(numeric) && value.trim() !== "" ? numeric : value;
137
+ }
138
+
139
+ function jsonResponseFromPostgrest(result: {
140
+ data: unknown;
141
+ error: unknown;
142
+ status: number;
143
+ }): Response {
144
+ if (result.error) {
145
+ return jsonResponse({ error: result.error }, result.status);
146
+ }
147
+ return jsonResponse(result.data, result.status);
148
+ }
149
+
150
+ function jsonResponse(body: unknown, status: number): Response {
151
+ return new Response(JSON.stringify(body), {
152
+ status,
153
+ headers: {
154
+ "content-type": "application/json",
155
+ },
156
+ });
157
+ }
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { SupabaseShimClient } from "./client.js";
2
+ import { handleSupabaseShimRequest } from "./handler.js";
3
+ import { PGliteShimDatabase } from "./pglite-database.js";
4
+ import type { SnapshotStorage, SupabaseShimDatabase } from "./types.js";
5
+
6
+ export { SupabaseShimAuthClient } from "./auth.js";
7
+ export { SupabaseShimClient } from "./client.js";
8
+ export { handleSupabaseShimRequest } from "./handler.js";
9
+ export { PGliteShimDatabase } from "./pglite-database.js";
10
+ export { SupabasePostgrestBuilder } from "./query-builder.js";
11
+ export { InMemorySnapshotStorage } from "./snapshot.js";
12
+ export type {
13
+ AuthChangeEvent,
14
+ PostgrestResponse,
15
+ QueryFilter,
16
+ QueryOrder,
17
+ SelectQuery,
18
+ ShimRow,
19
+ ShimSession,
20
+ ShimUser,
21
+ ShimValue,
22
+ SnapshotStorage,
23
+ SupabaseAuthResponse,
24
+ SupabaseShimDatabase,
25
+ SupabaseShimError,
26
+ } from "./types.js";
27
+
28
+ export interface SupabaseShimOptions {
29
+ initSql?: string;
30
+ database?: SupabaseShimDatabase;
31
+ snapshotStorage?: SnapshotStorage;
32
+ snapshotKey?: string;
33
+ basePath?: string;
34
+ }
35
+
36
+ export interface SupabaseShim {
37
+ client: SupabaseShimClient;
38
+ database: SupabaseShimDatabase;
39
+ snapshot(): Promise<void>;
40
+ restore(): Promise<boolean>;
41
+ handleRequest(request: Request): Promise<Response>;
42
+ close(): Promise<void>;
43
+ }
44
+
45
+ export async function createSupabaseShim(options: SupabaseShimOptions = {}): Promise<SupabaseShim> {
46
+ const database = options.database ?? (await PGliteShimDatabase.create());
47
+ if (options.initSql) {
48
+ await bootstrapShimDatabaseFromInitSql(database, options.initSql);
49
+ }
50
+
51
+ const client = new SupabaseShimClient(database);
52
+ const snapshotKey = options.snapshotKey ?? "default";
53
+
54
+ return {
55
+ client,
56
+ database,
57
+ async snapshot() {
58
+ if (!options.snapshotStorage) {
59
+ return;
60
+ }
61
+ await options.snapshotStorage.set(snapshotKey, await database.snapshot());
62
+ },
63
+ async restore() {
64
+ if (!options.snapshotStorage) {
65
+ return false;
66
+ }
67
+ const snapshot = await options.snapshotStorage.get(snapshotKey);
68
+ if (snapshot === null) {
69
+ return false;
70
+ }
71
+ await database.restore(snapshot);
72
+ return true;
73
+ },
74
+ handleRequest(request: Request) {
75
+ return handleSupabaseShimRequest(request, client, options.basePath);
76
+ },
77
+ async close() {
78
+ await database.close?.();
79
+ },
80
+ };
81
+ }
82
+
83
+ export async function bootstrapPgliteFromInitSql(
84
+ database: SupabaseShimDatabase,
85
+ initSql: string,
86
+ ): Promise<void> {
87
+ await bootstrapShimDatabaseFromInitSql(database, initSql);
88
+ }
89
+
90
+ export async function bootstrapShimDatabaseFromInitSql(
91
+ database: SupabaseShimDatabase,
92
+ initSql: string,
93
+ ): Promise<void> {
94
+ await database.bootstrap(initSql);
95
+ }