@staticbot/base44-supabase-shim 0.3.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/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { createClient as createSupabase, type SupabaseClient } from '@supabase/supabase-js';
2
+ import { makeAuth, type AuthOptions } from './auth.js';
3
+ import { makeEntitiesProxy } from './entities.js';
4
+ import { makeFunctions } from './functions.js';
5
+ import { makeIntegrations, type IntegrationsOptions } from './integrations.js';
6
+ import { app, makeAppLogs, makeUsers } from './misc.js';
7
+ import { makeStorage } from './storage.js';
8
+ import type { ClientOptions, EntitiesProxy } from './types.js';
9
+
10
+ export type * from './types.js';
11
+ export { defaultEntityToTable, resolveEntityMapping, parseOrderBy } from './entities.js';
12
+
13
+ export interface ExtendedClientOptions extends ClientOptions {
14
+ /** Auth-related options (login redirect path, etc). */
15
+ authOptions?: AuthOptions;
16
+ /** Integration stub configuration (storage default bucket, edge function names). */
17
+ integrations?: Partial<IntegrationsOptions>;
18
+ }
19
+
20
+ export interface Base44Client {
21
+ /** Underlying Supabase client (anon key). */
22
+ supabase: SupabaseClient;
23
+ entities: EntitiesProxy;
24
+ auth: ReturnType<typeof makeAuth>;
25
+ functions: ReturnType<typeof makeFunctions>;
26
+ storage: ReturnType<typeof makeStorage>;
27
+ integrations: ReturnType<typeof makeIntegrations>;
28
+ /** Per-page activity logger. Writes to core.audit_log; errors are swallowed. */
29
+ appLogs: ReturnType<typeof makeAppLogs>;
30
+ /** Admin user management; browser-side calls throw (use Studio instead). */
31
+ users: ReturnType<typeof makeUsers>;
32
+ /** Empty placeholder for base44.app property access. */
33
+ app: Record<string, unknown>;
34
+ /** Service-role-scoped namespace for trusted server contexts. Throws if no service key supplied. */
35
+ asServiceRole: { entities: EntitiesProxy };
36
+ }
37
+
38
+ /** Create a Base44-compatible client backed by Supabase. */
39
+ export function createClient(options: ExtendedClientOptions): Base44Client {
40
+ if (!options.supabaseUrl) throw new Error('createClient: supabaseUrl is required');
41
+ if (!options.supabaseAnonKey) throw new Error('createClient: supabaseAnonKey is required');
42
+ if (!options.schemaPrefix) throw new Error('createClient: schemaPrefix is required');
43
+
44
+ const supabase =
45
+ options.client ?? createSupabase(options.supabaseUrl, options.supabaseAnonKey);
46
+
47
+ const entities = makeEntitiesProxy(supabase, {
48
+ schemaPrefix: options.schemaPrefix,
49
+ sharedSchema: options.sharedSchema,
50
+ sharedEntities: options.sharedEntities,
51
+ entityMap: options.entityMap,
52
+ });
53
+
54
+ const asServiceRole = (() => {
55
+ let serviceClient: SupabaseClient | null = null;
56
+ let serviceEntities: EntitiesProxy | null = null;
57
+ return {
58
+ get entities(): EntitiesProxy {
59
+ if (!options.supabaseServiceRoleKey) {
60
+ throw new Error(
61
+ 'asServiceRole.entities accessed but supabaseServiceRoleKey was not provided. ' +
62
+ 'Only set this in trusted server contexts (edge functions, scripts).',
63
+ );
64
+ }
65
+ if (!serviceClient) {
66
+ serviceClient = createSupabase(options.supabaseUrl, options.supabaseServiceRoleKey);
67
+ }
68
+ if (!serviceEntities) {
69
+ serviceEntities = makeEntitiesProxy(serviceClient, {
70
+ schemaPrefix: options.schemaPrefix,
71
+ sharedSchema: options.sharedSchema,
72
+ sharedEntities: options.sharedEntities,
73
+ entityMap: options.entityMap,
74
+ });
75
+ }
76
+ return serviceEntities;
77
+ },
78
+ };
79
+ })();
80
+
81
+ return {
82
+ supabase,
83
+ entities,
84
+ auth: makeAuth(supabase, options.authOptions),
85
+ functions: makeFunctions(supabase),
86
+ storage: makeStorage(supabase, options.schemaPrefix),
87
+ integrations: makeIntegrations(supabase, {
88
+ defaultBucket: options.integrations?.defaultBucket ?? options.schemaPrefix,
89
+ sendEmailFunction: options.integrations?.sendEmailFunction,
90
+ invokeLlmFunction: options.integrations?.invokeLlmFunction,
91
+ }),
92
+ appLogs: makeAppLogs(supabase, options.schemaPrefix),
93
+ users: makeUsers(),
94
+ app,
95
+ asServiceRole,
96
+ };
97
+ }
@@ -0,0 +1,94 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ export interface IntegrationsOptions {
4
+ /** Default storage bucket for UploadFile. Falls back to schemaPrefix from createClient. */
5
+ defaultBucket: string;
6
+ /**
7
+ * Edge function name to invoke for SendEmail. If unset, calls fail loudly.
8
+ * Implement this function in supabase/volumes/functions/send-email/.
9
+ */
10
+ sendEmailFunction?: string;
11
+ /**
12
+ * Edge function name to invoke for InvokeLLM. If unset, calls fail loudly
13
+ * (AI features are out of scope for the air-gapped self-host stack).
14
+ */
15
+ invokeLlmFunction?: string;
16
+ }
17
+
18
+ /**
19
+ * Stub of `base44.integrations.Core.*`. Three methods are implemented:
20
+ *
21
+ * UploadFile → delegates to Supabase Storage upload + returns {url, path}.
22
+ * SendEmail → invokes a configured edge function (or throws if none).
23
+ * InvokeLLM → invokes a configured edge function (or throws if none).
24
+ *
25
+ * Air-gapped LAN cannot call OpenAI directly; wire your own LLM endpoint
26
+ * (Ollama, internal API gateway, etc.) inside an edge function and pass the
27
+ * function name via createClient({ integrations: { invokeLlmFunction: 'llm-relay' } }).
28
+ */
29
+ export function makeIntegrations(client: SupabaseClient, opts: IntegrationsOptions) {
30
+ const Core = {
31
+ async UploadFile({
32
+ file,
33
+ bucket,
34
+ path,
35
+ contentType,
36
+ }: {
37
+ file: Blob | File | ArrayBuffer | Uint8Array;
38
+ bucket?: string;
39
+ path?: string;
40
+ contentType?: string;
41
+ }): Promise<{ url: string; path: string }> {
42
+ const b = bucket ?? opts.defaultBucket;
43
+ const p =
44
+ path ??
45
+ `uploads/${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${(file as File)?.name ?? 'file'}`;
46
+ const { error } = await client.storage.from(b).upload(p, file, {
47
+ contentType: contentType ?? (file as File)?.type,
48
+ upsert: false,
49
+ });
50
+ if (error) throw error;
51
+ const { data } = client.storage.from(b).getPublicUrl(p);
52
+ return { url: data.publicUrl, path: p };
53
+ },
54
+
55
+ async SendEmail(payload: {
56
+ to: string | string[];
57
+ subject: string;
58
+ body?: string;
59
+ html?: string;
60
+ from?: string;
61
+ }): Promise<{ ok: boolean }> {
62
+ if (!opts.sendEmailFunction) {
63
+ throw new Error(
64
+ 'SendEmail not configured. Set integrations.sendEmailFunction in createClient() ' +
65
+ 'and deploy a corresponding Supabase Edge Function (e.g. send-email).',
66
+ );
67
+ }
68
+ const { error } = await client.functions.invoke(opts.sendEmailFunction, { body: payload });
69
+ if (error) throw error;
70
+ return { ok: true };
71
+ },
72
+
73
+ async InvokeLLM(payload: {
74
+ prompt: string;
75
+ model?: string;
76
+ [key: string]: unknown;
77
+ }): Promise<unknown> {
78
+ if (!opts.invokeLlmFunction) {
79
+ throw new Error(
80
+ 'InvokeLLM not configured. AI features are disabled in this self-host build. ' +
81
+ 'Set integrations.invokeLlmFunction in createClient() and deploy an edge function ' +
82
+ 'that proxies to your LLM endpoint (Ollama / internal gateway).',
83
+ );
84
+ }
85
+ const { data, error } = await client.functions.invoke(opts.invokeLlmFunction, {
86
+ body: payload,
87
+ });
88
+ if (error) throw error;
89
+ return data;
90
+ },
91
+ };
92
+
93
+ return { Core };
94
+ }
package/src/misc.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * Stub for `base44.appLogs` — Base44 cloud's per-page activity tracker.
5
+ * Writes a row to core.audit_log if the table exists, otherwise silently
6
+ * succeeds. App code wraps this in .catch(() => {}) anyway, so any failure
7
+ * is non-fatal.
8
+ */
9
+ export function makeAppLogs(client: SupabaseClient, app: string) {
10
+ return {
11
+ async logUserInApp(pageName: string): Promise<void> {
12
+ try {
13
+ await client.schema('core' as never).from('audit_logs').insert({
14
+ app,
15
+ action: 'page_view',
16
+ module: pageName,
17
+ });
18
+ } catch {
19
+ // best-effort; analytics errors must never break the app
20
+ }
21
+ },
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Stub for `base44.users` — admin-only user management. Self-host equivalent
27
+ * uses Supabase Auth admin API but requires a service_role key, which the
28
+ * browser client doesn't have. So inviteUser here logs a warning and returns
29
+ * a benign error so callers can show a "not supported" message.
30
+ */
31
+ export function makeUsers() {
32
+ return {
33
+ async inviteUser(_args: { email: string; [key: string]: unknown }): Promise<never> {
34
+ throw new Error(
35
+ 'inviteUser is admin-only and not exposed to the browser shim. ' +
36
+ 'Use the Studio dashboard or an edge function with service_role to invite users.',
37
+ );
38
+ },
39
+ };
40
+ }
41
+
42
+ /** Empty placeholder for `base44.app` — kept defined so property access doesn't crash. */
43
+ export const app: Record<string, unknown> = {};
package/src/server.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { createClient as createSupabase, type SupabaseClient } from '@supabase/supabase-js';
2
+ import { makeEntitiesProxy } from './entities.js';
3
+ import type { ClientOptions, EntitiesProxy } from './types.js';
4
+
5
+ export type * from './types.js';
6
+
7
+ export interface ServerClient {
8
+ supabase: SupabaseClient;
9
+ entities: EntitiesProxy;
10
+ asServiceRole: { entities: EntitiesProxy };
11
+ }
12
+
13
+ /**
14
+ * Create a Base44-style client from an incoming HTTP Request, intended for
15
+ * Supabase Edge Functions (Deno) or any server runtime that has fetch Request.
16
+ *
17
+ * Honors the caller's Authorization header so RLS applies as the end user.
18
+ * `asServiceRole` uses the configured service_role key (bypasses RLS) for
19
+ * privileged operations — analogous to base44.asServiceRole.
20
+ */
21
+ export function createClientFromRequest(
22
+ req: Request,
23
+ options: Omit<ClientOptions, 'client'> & { supabaseServiceRoleKey: string },
24
+ ): ServerClient {
25
+ if (!options.supabaseUrl) throw new Error('createClientFromRequest: supabaseUrl is required');
26
+ if (!options.supabaseAnonKey)
27
+ throw new Error('createClientFromRequest: supabaseAnonKey is required');
28
+ if (!options.supabaseServiceRoleKey)
29
+ throw new Error('createClientFromRequest: supabaseServiceRoleKey is required for server use');
30
+
31
+ const authHeader = req.headers.get('Authorization') ?? '';
32
+ const userClient = createSupabase(options.supabaseUrl, options.supabaseAnonKey, {
33
+ global: { headers: { Authorization: authHeader } },
34
+ auth: { persistSession: false, autoRefreshToken: false },
35
+ });
36
+ const serviceClient = createSupabase(options.supabaseUrl, options.supabaseServiceRoleKey, {
37
+ auth: { persistSession: false, autoRefreshToken: false },
38
+ });
39
+
40
+ const mappingOpts = {
41
+ schemaPrefix: options.schemaPrefix,
42
+ sharedSchema: options.sharedSchema,
43
+ sharedEntities: options.sharedEntities,
44
+ entityMap: options.entityMap,
45
+ };
46
+
47
+ return {
48
+ supabase: userClient,
49
+ entities: makeEntitiesProxy(userClient, mappingOpts),
50
+ asServiceRole: {
51
+ entities: makeEntitiesProxy(serviceClient, mappingOpts),
52
+ },
53
+ };
54
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ export interface UploadFileArgs {
4
+ /** Supabase Storage bucket name. Defaults to schemaPrefix from client options. */
5
+ bucket?: string;
6
+ /** Object path within the bucket (e.g. 'invoices/2026/inv-001.pdf'). */
7
+ path: string;
8
+ /** File contents. */
9
+ file: Blob | File | ArrayBuffer | Uint8Array;
10
+ /** Content-Type. Inferred from File if omitted. */
11
+ contentType?: string;
12
+ /** Overwrite existing object at path. Default false. */
13
+ upsert?: boolean;
14
+ }
15
+
16
+ export function makeStorage(client: SupabaseClient, defaultBucket: string) {
17
+ return {
18
+ /** Upload a file to a bucket. Returns the public URL. */
19
+ async uploadFile({
20
+ bucket,
21
+ path,
22
+ file,
23
+ contentType,
24
+ upsert = false,
25
+ }: UploadFileArgs): Promise<{ path: string; url: string }> {
26
+ const b = bucket ?? defaultBucket;
27
+ const { error } = await client.storage.from(b).upload(path, file, {
28
+ contentType,
29
+ upsert,
30
+ });
31
+ if (error) throw error;
32
+ const { data } = client.storage.from(b).getPublicUrl(path);
33
+ return { path, url: data.publicUrl };
34
+ },
35
+ /** Get a public URL for a stored object. */
36
+ getPublicUrl(path: string, bucket?: string): string {
37
+ const b = bucket ?? defaultBucket;
38
+ const { data } = client.storage.from(b).getPublicUrl(path);
39
+ return data.publicUrl;
40
+ },
41
+ /** Generate a time-limited signed URL for private buckets. */
42
+ async createSignedUrl(path: string, expiresInSec = 3600, bucket?: string): Promise<string> {
43
+ const b = bucket ?? defaultBucket;
44
+ const { data, error } = await client.storage.from(b).createSignedUrl(path, expiresInSec);
45
+ if (error) throw error;
46
+ return data.signedUrl;
47
+ },
48
+ /** Delete one or more objects. */
49
+ async remove(paths: string | string[], bucket?: string): Promise<void> {
50
+ const b = bucket ?? defaultBucket;
51
+ const list = Array.isArray(paths) ? paths : [paths];
52
+ const { error } = await client.storage.from(b).remove(list);
53
+ if (error) throw error;
54
+ },
55
+ /** List objects in a bucket prefix. */
56
+ async list(prefix?: string, bucket?: string) {
57
+ const b = bucket ?? defaultBucket;
58
+ const { data, error } = await client.storage.from(b).list(prefix);
59
+ if (error) throw error;
60
+ return data ?? [];
61
+ },
62
+ };
63
+ }
package/src/types.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
4
+
5
+ export interface EntityMapping {
6
+ schema: string;
7
+ table: string;
8
+ }
9
+
10
+ export interface ClientOptions {
11
+ /** Supabase project URL, e.g. https://api.erp.local */
12
+ supabaseUrl: string;
13
+ /** anon public key for browser use */
14
+ supabaseAnonKey: string;
15
+ /** service_role key — only set in trusted server contexts */
16
+ supabaseServiceRoleKey?: string;
17
+ /**
18
+ * Postgres schema for app-specific entities.
19
+ * E.g. 'propertyflow' / 'fms' / 'finance' / 'construction'.
20
+ */
21
+ schemaPrefix: string;
22
+ /**
23
+ * Postgres schema for shared entities (Customer, Company, User, Role...).
24
+ * Default: 'core'.
25
+ */
26
+ sharedSchema?: string;
27
+ /**
28
+ * List of entity names that live in `sharedSchema` instead of `schemaPrefix`.
29
+ * Default: ['Customer', 'Company', 'User', 'Role', 'Department', 'Notification', 'AuditLog'].
30
+ */
31
+ sharedEntities?: string[];
32
+ /**
33
+ * Explicit per-entity mapping overrides. Use this if the auto-derived
34
+ * snake_case+pluralize naming doesn't match the actual table name.
35
+ */
36
+ entityMap?: Record<string, EntityMapping>;
37
+ /**
38
+ * Optional pre-built supabase client. If omitted, one is created from the keys.
39
+ * Useful in tests / when you already have a client elsewhere.
40
+ */
41
+ client?: SupabaseClient;
42
+ }
43
+
44
+ export interface OrderBy {
45
+ /** Field name. Prefix with '-' for descending (Base44 convention). */
46
+ field: string;
47
+ ascending?: boolean;
48
+ }
49
+
50
+ /** Where-clause filter object as used by Base44.
51
+ *
52
+ * Three accepted shapes per field, all translated to PostgREST:
53
+ *
54
+ * 1. Equality / array shorthand: `{ status: 'active', id: ['a','b'] }`
55
+ * 2. Base44 SDK Mongo operators: `{ price: { $gte: 10, $lt: 100 }, status: { $in: ['a','b'] } }`
56
+ * 3. Legacy {op,value} form: `{ amount: { op: 'gt', value: 100 } }`
57
+ *
58
+ * Shape (2) is what `@base44/sdk` emits — its `entities.X.filter(q, ...)`
59
+ * forwards `q` to Base44's REST as URL-encoded JSON with $-prefixed ops.
60
+ * Apps that ran against Base44's managed backend pass these exact shapes.
61
+ */
62
+ export type MongoFilter = {
63
+ $eq?: unknown; $ne?: unknown;
64
+ $gt?: unknown; $gte?: unknown; $lt?: unknown; $lte?: unknown;
65
+ $in?: unknown[]; $nin?: unknown[];
66
+ $like?: string; $ilike?: string;
67
+ };
68
+
69
+ export type FilterValue =
70
+ | string
71
+ | number
72
+ | boolean
73
+ | null
74
+ | string[]
75
+ | number[]
76
+ | MongoFilter
77
+ | { op: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'in'; value: unknown };
78
+
79
+ export type FilterObject = Record<string, FilterValue>;
80
+
81
+ export interface EntityApi<T = Record<string, unknown>> {
82
+ list(orderBy?: string | OrderBy, limit?: number, skip?: number): Promise<T[]>;
83
+ filter(where: FilterObject, orderBy?: string | OrderBy, limit?: number, skip?: number): Promise<T[]>;
84
+ get(id: string): Promise<T | null>;
85
+ /** Alias for get() — Base44 SDK exposes both names. */
86
+ read(id: string): Promise<T | null>;
87
+ create(body: Partial<T>): Promise<T>;
88
+ /** Insert multiple rows in one call. */
89
+ bulkCreate(rows: Array<Partial<T>>): Promise<T[]>;
90
+ /** Update many rows by id; each row must contain its own `id`. */
91
+ bulkUpdate(rows: Array<Partial<T> & { id: string }>): Promise<T[]>;
92
+ update(id: string, body: Partial<T>): Promise<T>;
93
+ /**
94
+ * Apply the same patch to every row matching `where`. Accepts either a flat
95
+ * patch or a MongoDB-style `{ $set: {...} }` envelope (the Base44 SDK form).
96
+ */
97
+ updateMany(where: FilterObject, update: Partial<T> | { $set?: Partial<T> }): Promise<T[]>;
98
+ delete(id: string): Promise<void>;
99
+ /** Delete every row matching `where`. */
100
+ deleteMany(where: FilterObject): Promise<void>;
101
+ /**
102
+ * Subscribe to row changes via Supabase Realtime.
103
+ * Returns an unsubscribe function (matches Base44 convention).
104
+ */
105
+ subscribe(callback: (event: { type: 'insert' | 'update' | 'delete'; new?: T; old?: T }) => void): () => void;
106
+ }
107
+
108
+ export type EntitiesProxy = {
109
+ [entityName: string]: EntityApi;
110
+ };