@ankhorage/supabase-db 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/dist/adapter.d.ts +3 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +160 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/admin.d.ts +3 -0
  9. package/dist/admin.d.ts.map +1 -0
  10. package/dist/admin.js +148 -0
  11. package/dist/admin.js.map +1 -0
  12. package/dist/errors.d.ts +5 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +37 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +4 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/query.d.ts +4 -0
  21. package/dist/query.d.ts.map +1 -0
  22. package/dist/query.js +94 -0
  23. package/dist/query.js.map +1 -0
  24. package/dist/realtime.d.ts +10 -0
  25. package/dist/realtime.d.ts.map +1 -0
  26. package/dist/realtime.js +95 -0
  27. package/dist/realtime.js.map +1 -0
  28. package/dist/types.d.ts +117 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/types.js +2 -0
  31. package/dist/types.js.map +1 -0
  32. package/dist/validation.d.ts +7 -0
  33. package/dist/validation.d.ts.map +1 -0
  34. package/dist/validation.js +43 -0
  35. package/dist/validation.js.map +1 -0
  36. package/package.json +82 -0
  37. package/src/adapter.test.ts +173 -0
  38. package/src/adapter.ts +249 -0
  39. package/src/admin.test.ts +102 -0
  40. package/src/admin.ts +205 -0
  41. package/src/errors.ts +66 -0
  42. package/src/index.ts +23 -0
  43. package/src/query.ts +122 -0
  44. package/src/realtime.test.ts +178 -0
  45. package/src/realtime.ts +145 -0
  46. package/src/types.ts +145 -0
  47. package/src/validation.ts +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createSupabaseDbAdapter } from './adapter.js';
2
+ export { createSupabaseDbAdminAdapter } from './admin.js';
3
+ export { normalizeRealtimeEvent } from './realtime.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC","sourcesContent":["export { createSupabaseDbAdapter } from './adapter.js';\nexport { createSupabaseDbAdminAdapter } from './admin.js';\nexport { normalizeRealtimeEvent } from './realtime.js';\nexport type {\n DbAdminResult,\n DbAdminSqlExecutionResult,\n DbChangeEvent,\n DbChangeKind,\n DbChangeListener,\n DbCollectionSubscriptionInput,\n DbRecordSubscriptionInput,\n DbSubscription,\n RealtimeDbAdapter,\n SupabaseCollectionDefinition,\n SupabaseDbAdapter,\n SupabaseDbAdapterOptions,\n SupabaseDbAdminAdapter,\n SupabaseDbAdminAdapterOptions,\n SupabaseFieldDefinition,\n SupabaseFieldType,\n SupabaseRealtimeClient,\n SupabaseRealtimePayload,\n} from './types.js';\n"]}
@@ -0,0 +1,4 @@
1
+ import type { DbFilter, DbSelectInput } from '@ankhorage/contracts/db';
2
+ export declare function buildSelectUrl(baseUrl: string, input: DbSelectInput): URL;
3
+ export declare function buildMutationUrl(baseUrl: string, tableValue: string, filters: readonly DbFilter[]): URL;
4
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAU,aAAa,EAAU,MAAM,yBAAyB,CAAC;AAIvF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,GAAG,CAWzE;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,SAAS,QAAQ,EAAE,GAC3B,GAAG,CAOL"}
package/dist/query.js ADDED
@@ -0,0 +1,94 @@
1
+ import { validateIdentifier } from './validation.js';
2
+ export function buildSelectUrl(baseUrl, input) {
3
+ const table = validateIdentifier(input.table, 'Table');
4
+ const url = new URL(`${baseUrl}/rest/v1/${table}`);
5
+ url.searchParams.set('select', formatColumns(input.columns));
6
+ applyFilters(url, input.filters ?? []);
7
+ applySort(url, input.sort ?? []);
8
+ applyPage(url, input.page);
9
+ return url;
10
+ }
11
+ export function buildMutationUrl(baseUrl, tableValue, filters) {
12
+ const table = validateIdentifier(tableValue, 'Table');
13
+ const url = new URL(`${baseUrl}/rest/v1/${table}`);
14
+ applyFilters(url, filters);
15
+ return url;
16
+ }
17
+ function applyFilters(url, filters) {
18
+ for (const filter of filters) {
19
+ const field = validateIdentifier(filter.field, 'Filter field');
20
+ url.searchParams.append(field, formatFilter(filter));
21
+ }
22
+ }
23
+ function applySort(url, sort) {
24
+ if (sort.length === 0) {
25
+ return;
26
+ }
27
+ const order = sort
28
+ .map((item) => {
29
+ const field = validateIdentifier(item.field, 'Sort field');
30
+ return `${field}.${item.direction ?? 'asc'}`;
31
+ })
32
+ .join(',');
33
+ url.searchParams.set('order', order);
34
+ }
35
+ function applyPage(url, page) {
36
+ if (page?.limit !== undefined) {
37
+ url.searchParams.set('limit', String(page.limit));
38
+ }
39
+ if (page?.offset !== undefined) {
40
+ url.searchParams.set('offset', String(page.offset));
41
+ }
42
+ }
43
+ function formatColumns(columns) {
44
+ if (columns === undefined || columns.length === 0) {
45
+ return '*';
46
+ }
47
+ return columns.map((column) => validateIdentifier(column, 'Column')).join(',');
48
+ }
49
+ function formatFilter(filter) {
50
+ switch (filter.operator) {
51
+ case 'eq':
52
+ return `eq.${formatValue(filter.value)}`;
53
+ case 'neq':
54
+ return `neq.${formatValue(filter.value)}`;
55
+ case 'gt':
56
+ return `gt.${formatValue(filter.value)}`;
57
+ case 'gte':
58
+ return `gte.${formatValue(filter.value)}`;
59
+ case 'lt':
60
+ return `lt.${formatValue(filter.value)}`;
61
+ case 'lte':
62
+ return `lte.${formatValue(filter.value)}`;
63
+ case 'in':
64
+ return `in.(${formatInValue(filter.value)})`;
65
+ case 'contains':
66
+ return `cs.${formatValue(filter.value)}`;
67
+ case 'startsWith':
68
+ return `like.${formatLikeValue(filter.value)}*`;
69
+ case 'endsWith':
70
+ return `like.*${formatLikeValue(filter.value)}`;
71
+ }
72
+ }
73
+ function formatInValue(value) {
74
+ if (!Array.isArray(value)) {
75
+ return formatValue(value);
76
+ }
77
+ return value.map((item) => formatValue(item)).join(',');
78
+ }
79
+ function formatLikeValue(value) {
80
+ return String(value).replaceAll('*', '\\*');
81
+ }
82
+ function formatValue(value) {
83
+ if (value === null) {
84
+ return 'null';
85
+ }
86
+ if (typeof value === 'string') {
87
+ return value;
88
+ }
89
+ if (typeof value === 'number' || typeof value === 'boolean') {
90
+ return String(value);
91
+ }
92
+ return JSON.stringify(value);
93
+ }
94
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DbFilter, DbPage, DbSelectInput, DbSort } from '@ankhorage/contracts/db';\n\nimport { validateIdentifier } from './validation.js';\n\nexport function buildSelectUrl(baseUrl: string, input: DbSelectInput): URL {\n const table = validateIdentifier(input.table, 'Table');\n const url = new URL(`${baseUrl}/rest/v1/${table}`);\n\n url.searchParams.set('select', formatColumns(input.columns));\n\n applyFilters(url, input.filters ?? []);\n applySort(url, input.sort ?? []);\n applyPage(url, input.page);\n\n return url;\n}\n\nexport function buildMutationUrl(\n baseUrl: string,\n tableValue: string,\n filters: readonly DbFilter[],\n): URL {\n const table = validateIdentifier(tableValue, 'Table');\n const url = new URL(`${baseUrl}/rest/v1/${table}`);\n\n applyFilters(url, filters);\n\n return url;\n}\n\nfunction applyFilters(url: URL, filters: readonly DbFilter[]): void {\n for (const filter of filters) {\n const field = validateIdentifier(filter.field, 'Filter field');\n url.searchParams.append(field, formatFilter(filter));\n }\n}\n\nfunction applySort(url: URL, sort: readonly DbSort[]): void {\n if (sort.length === 0) {\n return;\n }\n\n const order = sort\n .map((item) => {\n const field = validateIdentifier(item.field, 'Sort field');\n return `${field}.${item.direction ?? 'asc'}`;\n })\n .join(',');\n\n url.searchParams.set('order', order);\n}\n\nfunction applyPage(url: URL, page: DbPage | undefined): void {\n if (page?.limit !== undefined) {\n url.searchParams.set('limit', String(page.limit));\n }\n\n if (page?.offset !== undefined) {\n url.searchParams.set('offset', String(page.offset));\n }\n}\n\nfunction formatColumns(columns: readonly string[] | undefined): string {\n if (columns === undefined || columns.length === 0) {\n return '*';\n }\n\n return columns.map((column) => validateIdentifier(column, 'Column')).join(',');\n}\n\nfunction formatFilter(filter: DbFilter): string {\n switch (filter.operator) {\n case 'eq':\n return `eq.${formatValue(filter.value)}`;\n case 'neq':\n return `neq.${formatValue(filter.value)}`;\n case 'gt':\n return `gt.${formatValue(filter.value)}`;\n case 'gte':\n return `gte.${formatValue(filter.value)}`;\n case 'lt':\n return `lt.${formatValue(filter.value)}`;\n case 'lte':\n return `lte.${formatValue(filter.value)}`;\n case 'in':\n return `in.(${formatInValue(filter.value)})`;\n case 'contains':\n return `cs.${formatValue(filter.value)}`;\n case 'startsWith':\n return `like.${formatLikeValue(filter.value)}*`;\n case 'endsWith':\n return `like.*${formatLikeValue(filter.value)}`;\n }\n}\n\nfunction formatInValue(value: unknown): string {\n if (!Array.isArray(value)) {\n return formatValue(value);\n }\n\n return value.map((item) => formatValue(item)).join(',');\n}\n\nfunction formatLikeValue(value: unknown): string {\n return String(value).replaceAll('*', '\\\\*');\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n\n if (typeof value === 'string') {\n return value;\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n\n return JSON.stringify(value);\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { DbRecord } from '@ankhorage/contracts/db';
2
+ import type { DbChangeEvent, RealtimeDbAdapter, SupabaseRealtimeClient, SupabaseRealtimePayload } from './types.js';
3
+ interface RealtimeApiConfig {
4
+ readonly client: SupabaseRealtimeClient;
5
+ readonly defaultSchema: string;
6
+ }
7
+ export declare function createRealtimeApi(config: RealtimeApiConfig): RealtimeDbAdapter['realtime'];
8
+ export declare function normalizeRealtimeEvent<TRecord extends object = DbRecord>(payload: SupabaseRealtimePayload, fallbackTable: string, fallbackSchema: string): DbChangeEvent<TRecord> | null;
9
+ export {};
10
+ //# sourceMappingURL=realtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.d.ts","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAExD,OAAO,KAAK,EACV,aAAa,EAMb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAoE1F;AAED,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,MAAM,GAAG,QAAQ,EACtE,OAAO,EAAE,uBAAuB,EAChC,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,aAAa,CAAC,OAAO,CAAC,GAAG,IAAI,CAyB/B"}
@@ -0,0 +1,95 @@
1
+ import { validateIdentifier } from './validation.js';
2
+ export function createRealtimeApi(config) {
3
+ return {
4
+ subscribeToCollection(input, listener) {
5
+ const schema = input.schema ?? config.defaultSchema;
6
+ const table = validateIdentifier(input.table, 'Realtime table');
7
+ const channel = config.client
8
+ .channel(`ankhorage-db:${schema}:${table}`)
9
+ .on('postgres_changes', {
10
+ event: '*',
11
+ schema,
12
+ table,
13
+ }, (payload) => {
14
+ const event = normalizeRealtimeEvent(payload, table, schema);
15
+ if (event !== null) {
16
+ listener(event);
17
+ }
18
+ })
19
+ .subscribe();
20
+ return {
21
+ async unsubscribe() {
22
+ await config.client.removeChannel(channel);
23
+ },
24
+ };
25
+ },
26
+ subscribeToRecord(input, listener) {
27
+ const schema = input.schema ?? config.defaultSchema;
28
+ const table = validateIdentifier(input.table, 'Realtime table');
29
+ const idField = validateIdentifier(input.idField ?? 'id', 'Realtime record id field');
30
+ const channel = config.client
31
+ .channel(`ankhorage-db:${schema}:${table}:${idField}:${String(input.id)}`)
32
+ .on('postgres_changes', {
33
+ event: '*',
34
+ schema,
35
+ table,
36
+ filter: `${idField}=eq.${String(input.id)}`,
37
+ }, (payload) => {
38
+ const event = normalizeRealtimeEvent(payload, table, schema);
39
+ if (event !== null) {
40
+ listener(event);
41
+ }
42
+ })
43
+ .subscribe();
44
+ return {
45
+ async unsubscribe() {
46
+ await config.client.removeChannel(channel);
47
+ },
48
+ };
49
+ },
50
+ };
51
+ }
52
+ export function normalizeRealtimeEvent(payload, fallbackTable, fallbackSchema) {
53
+ const kind = normalizeKind(payload.eventType);
54
+ if (kind === null) {
55
+ return null;
56
+ }
57
+ const record = normalizeRecord(payload.new);
58
+ const previousRecord = normalizeRecord(payload.old);
59
+ const base = {
60
+ table: payload.table ?? fallbackTable,
61
+ schema: payload.schema ?? fallbackSchema,
62
+ kind,
63
+ record: kind === 'delete' ? null : record,
64
+ committedAt: payload.commit_timestamp,
65
+ };
66
+ if (previousRecord === null) {
67
+ return base;
68
+ }
69
+ return {
70
+ ...base,
71
+ previousRecord,
72
+ };
73
+ }
74
+ function normalizeKind(value) {
75
+ switch (value) {
76
+ case 'INSERT':
77
+ return 'insert';
78
+ case 'UPDATE':
79
+ return 'update';
80
+ case 'DELETE':
81
+ return 'delete';
82
+ default:
83
+ return null;
84
+ }
85
+ }
86
+ function normalizeRecord(value) {
87
+ if (!isRecord(value)) {
88
+ return null;
89
+ }
90
+ return value;
91
+ }
92
+ function isRecord(value) {
93
+ return typeof value === 'object' && value !== null;
94
+ }
95
+ //# sourceMappingURL=realtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime.js","sourceRoot":"","sources":["../src/realtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DbRecord } from '@ankhorage/contracts/db';\n\nimport type {\n DbChangeEvent,\n DbChangeKind,\n DbChangeListener,\n DbCollectionSubscriptionInput,\n DbRecordSubscriptionInput,\n DbSubscription,\n RealtimeDbAdapter,\n SupabaseRealtimeClient,\n SupabaseRealtimePayload,\n} from './types.js';\nimport { validateIdentifier } from './validation.js';\n\ninterface RealtimeApiConfig {\n readonly client: SupabaseRealtimeClient;\n readonly defaultSchema: string;\n}\n\nexport function createRealtimeApi(config: RealtimeApiConfig): RealtimeDbAdapter['realtime'] {\n return {\n subscribeToCollection<TRecord extends object = DbRecord>(\n input: DbCollectionSubscriptionInput,\n listener: DbChangeListener<TRecord>,\n ): DbSubscription {\n const schema = input.schema ?? config.defaultSchema;\n const table = validateIdentifier(input.table, 'Realtime table');\n const channel = config.client\n .channel(`ankhorage-db:${schema}:${table}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema,\n table,\n },\n (payload) => {\n const event = normalizeRealtimeEvent<TRecord>(payload, table, schema);\n\n if (event !== null) {\n listener(event);\n }\n },\n )\n .subscribe();\n\n return {\n async unsubscribe(): Promise<void> {\n await config.client.removeChannel(channel);\n },\n };\n },\n\n subscribeToRecord<TRecord extends object = DbRecord>(\n input: DbRecordSubscriptionInput,\n listener: DbChangeListener<TRecord>,\n ): DbSubscription {\n const schema = input.schema ?? config.defaultSchema;\n const table = validateIdentifier(input.table, 'Realtime table');\n const idField = validateIdentifier(input.idField ?? 'id', 'Realtime record id field');\n const channel = config.client\n .channel(`ankhorage-db:${schema}:${table}:${idField}:${String(input.id)}`)\n .on(\n 'postgres_changes',\n {\n event: '*',\n schema,\n table,\n filter: `${idField}=eq.${String(input.id)}`,\n },\n (payload) => {\n const event = normalizeRealtimeEvent<TRecord>(payload, table, schema);\n\n if (event !== null) {\n listener(event);\n }\n },\n )\n .subscribe();\n\n return {\n async unsubscribe(): Promise<void> {\n await config.client.removeChannel(channel);\n },\n };\n },\n };\n}\n\nexport function normalizeRealtimeEvent<TRecord extends object = DbRecord>(\n payload: SupabaseRealtimePayload,\n fallbackTable: string,\n fallbackSchema: string,\n): DbChangeEvent<TRecord> | null {\n const kind = normalizeKind(payload.eventType);\n\n if (kind === null) {\n return null;\n }\n\n const record = normalizeRecord<TRecord>(payload.new);\n const previousRecord = normalizeRecord<TRecord>(payload.old);\n const base = {\n table: payload.table ?? fallbackTable,\n schema: payload.schema ?? fallbackSchema,\n kind,\n record: kind === 'delete' ? null : record,\n committedAt: payload.commit_timestamp,\n };\n\n if (previousRecord === null) {\n return base;\n }\n\n return {\n ...base,\n previousRecord,\n };\n}\n\nfunction normalizeKind(value: string | undefined): DbChangeKind | null {\n switch (value) {\n case 'INSERT':\n return 'insert';\n case 'UPDATE':\n return 'update';\n case 'DELETE':\n return 'delete';\n default:\n return null;\n }\n}\n\nfunction normalizeRecord<TRecord extends object>(value: unknown): TRecord | null {\n if (!isRecord<TRecord>(value)) {\n return null;\n }\n\n return value;\n}\n\nfunction isRecord<TRecord extends object>(value: unknown): value is TRecord {\n return typeof value === 'object' && value !== null;\n}\n"]}
@@ -0,0 +1,117 @@
1
+ import type { DbAdapter, DbRecord } from '@ankhorage/contracts/db';
2
+ interface SupabaseRealtimePostgresChangesFilter {
3
+ readonly event: 'INSERT' | 'UPDATE' | 'DELETE' | '*';
4
+ readonly schema: string;
5
+ readonly table: string;
6
+ readonly filter?: string;
7
+ }
8
+ export interface SupabaseRealtimeChannel {
9
+ on(type: 'postgres_changes', filter: SupabaseRealtimePostgresChangesFilter, callback: (payload: SupabaseRealtimePayload) => void): SupabaseRealtimeChannel;
10
+ subscribe(callback?: (status: string) => void): SupabaseRealtimeChannel;
11
+ unsubscribe(): Promise<'ok' | 'timed out' | 'error'>;
12
+ }
13
+ export interface SupabaseRealtimeClient {
14
+ channel(topic: string): SupabaseRealtimeChannel;
15
+ removeChannel(channel: SupabaseRealtimeChannel): Promise<'ok' | 'timed out' | 'error'>;
16
+ }
17
+ export interface SupabaseRealtimePayload {
18
+ eventType?: string;
19
+ schema?: string;
20
+ table?: string;
21
+ commit_timestamp?: string;
22
+ new?: unknown;
23
+ old?: unknown;
24
+ errors?: string[] | null;
25
+ }
26
+ export interface SupabaseDbAdapterOptions {
27
+ readonly url: string;
28
+ readonly anonKey: string;
29
+ readonly schema?: string;
30
+ readonly fetch?: typeof fetch;
31
+ readonly realtime?: boolean;
32
+ readonly realtimeClient?: SupabaseRealtimeClient;
33
+ }
34
+ export interface SupabaseDbAdminAdapterOptions {
35
+ readonly url: string;
36
+ readonly serviceRoleKey?: string;
37
+ readonly schema?: string;
38
+ readonly execute?: boolean;
39
+ readonly executeSql?: (sql: string) => Promise<DbAdminSqlExecutionResult>;
40
+ }
41
+ export interface DbAdminSqlExecutionResult {
42
+ readonly ok: boolean;
43
+ readonly error?: {
44
+ readonly code: string;
45
+ readonly message: string;
46
+ readonly cause?: unknown;
47
+ };
48
+ }
49
+ export type SupabaseDbAdapter = DbAdapter & Partial<RealtimeDbAdapter>;
50
+ export interface RealtimeDbAdapter {
51
+ readonly realtime: {
52
+ subscribeToCollection<TRecord extends object = DbRecord>(input: DbCollectionSubscriptionInput, listener: DbChangeListener<TRecord>): DbSubscription;
53
+ subscribeToRecord<TRecord extends object = DbRecord>(input: DbRecordSubscriptionInput, listener: DbChangeListener<TRecord>): DbSubscription;
54
+ };
55
+ }
56
+ export interface DbCollectionSubscriptionInput {
57
+ readonly table: string;
58
+ readonly schema?: string;
59
+ }
60
+ export interface DbRecordSubscriptionInput extends DbCollectionSubscriptionInput {
61
+ readonly id: string | number;
62
+ readonly idField?: string;
63
+ }
64
+ export type DbChangeKind = 'insert' | 'update' | 'delete';
65
+ export interface DbChangeEvent<TRecord extends object = DbRecord> {
66
+ readonly table: string;
67
+ readonly schema: string;
68
+ readonly kind: DbChangeKind;
69
+ readonly record: TRecord | null;
70
+ readonly previousRecord?: TRecord;
71
+ readonly committedAt?: string;
72
+ }
73
+ export type DbChangeListener<TRecord extends object = DbRecord> = (event: DbChangeEvent<TRecord>) => void;
74
+ export interface DbSubscription {
75
+ unsubscribe(): Promise<void>;
76
+ }
77
+ export interface SupabaseCollectionDefinition {
78
+ readonly name: string;
79
+ readonly fields: readonly SupabaseFieldDefinition[];
80
+ readonly primaryKey?: string;
81
+ }
82
+ export type SupabaseFieldType = 'text' | 'number' | 'boolean' | 'datetime' | 'json' | 'uuid';
83
+ export interface SupabaseFieldDefinition {
84
+ readonly name: string;
85
+ readonly type: SupabaseFieldType;
86
+ readonly required?: boolean;
87
+ readonly unique?: boolean;
88
+ readonly defaultValue?: string | number | boolean | null;
89
+ }
90
+ export interface SupabaseDbAdminAdapter {
91
+ readonly capabilities: {
92
+ readonly supportsSchemaGeneration: true;
93
+ readonly supportsDirectExecution: boolean;
94
+ };
95
+ createCollection(input: SupabaseCollectionDefinition): Promise<DbAdminResult>;
96
+ deleteCollection(input: {
97
+ readonly name: string;
98
+ }): Promise<DbAdminResult>;
99
+ generateCreateCollectionSql(input: SupabaseCollectionDefinition): DbAdminResult;
100
+ generateDeleteCollectionSql(input: {
101
+ readonly name: string;
102
+ }): DbAdminResult;
103
+ }
104
+ export type DbAdminResult = {
105
+ readonly ok: true;
106
+ readonly sql: string;
107
+ readonly executed: boolean;
108
+ } | {
109
+ readonly ok: false;
110
+ readonly error: {
111
+ readonly code: string;
112
+ readonly message: string;
113
+ readonly cause?: unknown;
114
+ };
115
+ };
116
+ export {};
117
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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":"","sourcesContent":["import type { DbAdapter, DbRecord } from '@ankhorage/contracts/db';\n\ninterface SupabaseRealtimePostgresChangesFilter {\n readonly event: 'INSERT' | 'UPDATE' | 'DELETE' | '*';\n readonly schema: string;\n readonly table: string;\n readonly filter?: string;\n}\n\nexport interface SupabaseRealtimeChannel {\n on(\n type: 'postgres_changes',\n filter: SupabaseRealtimePostgresChangesFilter,\n callback: (payload: SupabaseRealtimePayload) => void,\n ): SupabaseRealtimeChannel;\n subscribe(callback?: (status: string) => void): SupabaseRealtimeChannel;\n unsubscribe(): Promise<'ok' | 'timed out' | 'error'>;\n}\n\nexport interface SupabaseRealtimeClient {\n channel(topic: string): SupabaseRealtimeChannel;\n removeChannel(channel: SupabaseRealtimeChannel): Promise<'ok' | 'timed out' | 'error'>;\n}\n\nexport interface SupabaseRealtimePayload {\n eventType?: string;\n schema?: string;\n table?: string;\n commit_timestamp?: string;\n new?: unknown;\n old?: unknown;\n errors?: string[] | null;\n}\n\nexport interface SupabaseDbAdapterOptions {\n readonly url: string;\n readonly anonKey: string;\n readonly schema?: string;\n readonly fetch?: typeof fetch;\n readonly realtime?: boolean;\n readonly realtimeClient?: SupabaseRealtimeClient;\n}\n\nexport interface SupabaseDbAdminAdapterOptions {\n readonly url: string;\n readonly serviceRoleKey?: string;\n readonly schema?: string;\n readonly execute?: boolean;\n readonly executeSql?: (sql: string) => Promise<DbAdminSqlExecutionResult>;\n}\n\nexport interface DbAdminSqlExecutionResult {\n readonly ok: boolean;\n readonly error?: {\n readonly code: string;\n readonly message: string;\n readonly cause?: unknown;\n };\n}\n\nexport type SupabaseDbAdapter = DbAdapter & Partial<RealtimeDbAdapter>;\n\nexport interface RealtimeDbAdapter {\n readonly realtime: {\n subscribeToCollection<TRecord extends object = DbRecord>(\n input: DbCollectionSubscriptionInput,\n listener: DbChangeListener<TRecord>,\n ): DbSubscription;\n subscribeToRecord<TRecord extends object = DbRecord>(\n input: DbRecordSubscriptionInput,\n listener: DbChangeListener<TRecord>,\n ): DbSubscription;\n };\n}\n\nexport interface DbCollectionSubscriptionInput {\n readonly table: string;\n readonly schema?: string;\n}\n\nexport interface DbRecordSubscriptionInput extends DbCollectionSubscriptionInput {\n readonly id: string | number;\n readonly idField?: string;\n}\n\nexport type DbChangeKind = 'insert' | 'update' | 'delete';\n\nexport interface DbChangeEvent<TRecord extends object = DbRecord> {\n readonly table: string;\n readonly schema: string;\n readonly kind: DbChangeKind;\n readonly record: TRecord | null;\n readonly previousRecord?: TRecord;\n readonly committedAt?: string;\n}\n\nexport type DbChangeListener<TRecord extends object = DbRecord> = (\n event: DbChangeEvent<TRecord>,\n) => void;\n\nexport interface DbSubscription {\n unsubscribe(): Promise<void>;\n}\n\nexport interface SupabaseCollectionDefinition {\n readonly name: string;\n readonly fields: readonly SupabaseFieldDefinition[];\n readonly primaryKey?: string;\n}\n\nexport type SupabaseFieldType = 'text' | 'number' | 'boolean' | 'datetime' | 'json' | 'uuid';\n\nexport interface SupabaseFieldDefinition {\n readonly name: string;\n readonly type: SupabaseFieldType;\n readonly required?: boolean;\n readonly unique?: boolean;\n readonly defaultValue?: string | number | boolean | null;\n}\n\nexport interface SupabaseDbAdminAdapter {\n readonly capabilities: {\n readonly supportsSchemaGeneration: true;\n readonly supportsDirectExecution: boolean;\n };\n createCollection(input: SupabaseCollectionDefinition): Promise<DbAdminResult>;\n deleteCollection(input: { readonly name: string }): Promise<DbAdminResult>;\n generateCreateCollectionSql(input: SupabaseCollectionDefinition): DbAdminResult;\n generateDeleteCollectionSql(input: { readonly name: string }): DbAdminResult;\n}\n\nexport type DbAdminResult =\n | {\n readonly ok: true;\n readonly sql: string;\n readonly executed: boolean;\n }\n | {\n readonly ok: false;\n readonly error: {\n readonly code: string;\n readonly message: string;\n readonly cause?: unknown;\n };\n };\n"]}
@@ -0,0 +1,7 @@
1
+ import type { DbFilter } from '@ankhorage/contracts/db';
2
+ export declare function validateUrl(url: string): string;
3
+ export declare function validateKey(value: string, label: string): string;
4
+ export declare function validateIdentifier(value: string, label: string): string;
5
+ export declare function validateFilters(filters: readonly DbFilter[], label: string): void;
6
+ export declare function quoteIdentifier(value: string): string;
7
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAExD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAc/C;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQjF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIrD"}
@@ -0,0 +1,43 @@
1
+ export function validateUrl(url) {
2
+ const trimmed = url.trim();
3
+ if (trimmed.length === 0) {
4
+ throw new TypeError('Supabase URL is required.');
5
+ }
6
+ try {
7
+ new URL(trimmed);
8
+ }
9
+ catch {
10
+ throw new TypeError('Supabase URL must be a valid URL.');
11
+ }
12
+ return trimmed.replace(/\/+$/, '');
13
+ }
14
+ export function validateKey(value, label) {
15
+ const trimmed = value.trim();
16
+ if (trimmed.length === 0) {
17
+ throw new TypeError(`${label} is required.`);
18
+ }
19
+ return trimmed;
20
+ }
21
+ export function validateIdentifier(value, label) {
22
+ const trimmed = value.trim();
23
+ if (trimmed.length === 0) {
24
+ throw new TypeError(`${label} is required.`);
25
+ }
26
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
27
+ throw new TypeError(`${label} must be a valid SQL identifier.`);
28
+ }
29
+ return trimmed;
30
+ }
31
+ export function validateFilters(filters, label) {
32
+ if (filters.length === 0) {
33
+ throw new TypeError(`${label} requires at least one filter.`);
34
+ }
35
+ for (const filter of filters) {
36
+ validateIdentifier(filter.field, 'Filter field');
37
+ }
38
+ }
39
+ export function quoteIdentifier(value) {
40
+ const identifier = validateIdentifier(value, 'SQL identifier');
41
+ return `"${identifier.replaceAll('"', '""')}"`;
42
+ }
43
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,2BAA2B,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,KAAa;IACtD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,eAAe,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,KAAa;IAC7D,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,eAAe,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAA4B,EAAE,KAAa;IACzE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,GAAG,KAAK,gCAAgC,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,kBAAkB,CAAC,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IAE/D,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;AACjD,CAAC","sourcesContent":["import type { DbFilter } from '@ankhorage/contracts/db';\n\nexport function validateUrl(url: string): string {\n const trimmed = url.trim();\n\n if (trimmed.length === 0) {\n throw new TypeError('Supabase URL is required.');\n }\n\n try {\n new URL(trimmed);\n } catch {\n throw new TypeError('Supabase URL must be a valid URL.');\n }\n\n return trimmed.replace(/\\/+$/, '');\n}\n\nexport function validateKey(value: string, label: string): string {\n const trimmed = value.trim();\n\n if (trimmed.length === 0) {\n throw new TypeError(`${label} is required.`);\n }\n\n return trimmed;\n}\n\nexport function validateIdentifier(value: string, label: string): string {\n const trimmed = value.trim();\n\n if (trimmed.length === 0) {\n throw new TypeError(`${label} is required.`);\n }\n\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {\n throw new TypeError(`${label} must be a valid SQL identifier.`);\n }\n\n return trimmed;\n}\n\nexport function validateFilters(filters: readonly DbFilter[], label: string): void {\n if (filters.length === 0) {\n throw new TypeError(`${label} requires at least one filter.`);\n }\n\n for (const filter of filters) {\n validateIdentifier(filter.field, 'Filter field');\n }\n}\n\nexport function quoteIdentifier(value: string): string {\n const identifier = validateIdentifier(value, 'SQL identifier');\n\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@ankhorage/supabase-db",
3
+ "type": "module",
4
+ "version": "0.2.0",
5
+ "description": "Supabase database adapter for Ankhorage contracts with CRUD, schema management, and optional realtime support.",
6
+ "homepage": "https://github.com/ankhorage/supabase-db#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/ankhorage/supabase-db/issues"
9
+ },
10
+ "license": "MIT",
11
+ "main": "./dist/index.js",
12
+ "sideEffects": false,
13
+ "types": "./dist/index.d.ts",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ankhorage/supabase-db.git"
17
+ },
18
+ "keywords": [
19
+ "typescript",
20
+ "supabase",
21
+ "database",
22
+ "postgres",
23
+ "crud",
24
+ "realtime",
25
+ "adapter"
26
+ ],
27
+ "dependencies": {
28
+ "@ankhorage/contracts": "^1.2.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@supabase/supabase-js": "^2.105.3"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@supabase/supabase-js": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "package.json",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
45
+ ],
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist/index.d.ts",
49
+ "import": "./dist/index.js",
50
+ "default": "./dist/index.js"
51
+ },
52
+ "./package.json": "./package.json"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
58
+ "build": "rm -rf dist tsconfig.tsbuildinfo && bun x tsc -p tsconfig.json",
59
+ "changeset": "changeset",
60
+ "changeset:status": "changeset status --since=origin/main",
61
+ "docs": "paradox",
62
+ "knip": "knip",
63
+ "lint": "ankhorage-eslint . --max-warnings=0",
64
+ "lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
65
+ "format": "ankhorage-prettier --write .",
66
+ "format:check": "ankhorage-prettier --check .",
67
+ "prepack": "bun run build",
68
+ "test": "bun test src",
69
+ "typecheck": "bun x tsc --noEmit -p tsconfig.json",
70
+ "version-packages": "changeset version"
71
+ },
72
+ "devDependencies": {
73
+ "@ankhorage/devtools": "^1.0.6",
74
+ "@ankhorage/paradox": "^0.1.6",
75
+ "@changesets/cli": "^2.31.0",
76
+ "@types/bun": "^1.3.13",
77
+ "@types/node": "^25.6.0",
78
+ "knip": "^6.12.2",
79
+ "typescript": "^5.9.3"
80
+ },
81
+ "packageManager": "bun@1.3.13"
82
+ }