@isomorph.ai/app-sdk 1.2.1
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/dist/index.d.ts +367 -0
- package/dist/index.js +602 -0
- package/dist/integration-execute-input-rules.d.ts +187 -0
- package/dist/integration-execute-input-rules.js +114 -0
- package/dist/supabase-compat.d.ts +323 -0
- package/dist/supabase-compat.js +308 -0
- package/dist/supabase-compat.typecheck.d.ts +23 -0
- package/dist/supabase-compat.typecheck.js +92 -0
- package/package.json +15 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-operation input rules of integration-execute-request: derived from each
|
|
3
|
+
* operation's then-block (propertyNames.enum, required, per-field overrides).
|
|
4
|
+
* The governance execute route, the app SDK and the kit's local check validate
|
|
5
|
+
* with validateIntegrationExecuteInput below, so a request is refused with the
|
|
6
|
+
* same message wherever it is made.
|
|
7
|
+
*/
|
|
8
|
+
export type IntegrationExecuteInputFieldRule = {
|
|
9
|
+
kind: 'integer';
|
|
10
|
+
required: boolean;
|
|
11
|
+
minimum: number;
|
|
12
|
+
maximum: number;
|
|
13
|
+
default?: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'string';
|
|
16
|
+
required: boolean;
|
|
17
|
+
minLength: number;
|
|
18
|
+
maxLength?: number;
|
|
19
|
+
pattern?: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'string[]';
|
|
22
|
+
required: boolean;
|
|
23
|
+
minItems: number;
|
|
24
|
+
maxItems: number;
|
|
25
|
+
uniqueItems: boolean;
|
|
26
|
+
item: {
|
|
27
|
+
minLength: number;
|
|
28
|
+
maxLength?: number;
|
|
29
|
+
pattern?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type IntegrationExecuteOperationRule = {
|
|
33
|
+
idempotencyKey: boolean;
|
|
34
|
+
fields: Record<string, IntegrationExecuteInputFieldRule>;
|
|
35
|
+
};
|
|
36
|
+
export declare const IntegrationExecuteInputRules: {
|
|
37
|
+
readonly "slack.channel.history": {
|
|
38
|
+
readonly idempotencyKey: false;
|
|
39
|
+
readonly fields: {
|
|
40
|
+
readonly limit: {
|
|
41
|
+
readonly kind: "integer";
|
|
42
|
+
readonly required: false;
|
|
43
|
+
readonly minimum: 1;
|
|
44
|
+
readonly maximum: 15;
|
|
45
|
+
readonly default: 15;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
readonly "slack.message.post": {
|
|
50
|
+
readonly idempotencyKey: true;
|
|
51
|
+
readonly fields: {
|
|
52
|
+
readonly text: {
|
|
53
|
+
readonly kind: "string";
|
|
54
|
+
readonly required: true;
|
|
55
|
+
readonly minLength: 1;
|
|
56
|
+
readonly maxLength: 4000;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
readonly "gmail.thread.list": {
|
|
61
|
+
readonly idempotencyKey: false;
|
|
62
|
+
readonly fields: {
|
|
63
|
+
readonly limit: {
|
|
64
|
+
readonly kind: "integer";
|
|
65
|
+
readonly required: false;
|
|
66
|
+
readonly minimum: 1;
|
|
67
|
+
readonly maximum: 15;
|
|
68
|
+
readonly default: 15;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
readonly "gmail.message.read": {
|
|
73
|
+
readonly idempotencyKey: false;
|
|
74
|
+
readonly fields: {
|
|
75
|
+
readonly messageId: {
|
|
76
|
+
readonly kind: "string";
|
|
77
|
+
readonly required: true;
|
|
78
|
+
readonly minLength: 0;
|
|
79
|
+
readonly pattern: "^[A-Za-z0-9_-]{1,64}$";
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
readonly "gmail.message.send": {
|
|
84
|
+
readonly idempotencyKey: true;
|
|
85
|
+
readonly fields: {
|
|
86
|
+
readonly to: {
|
|
87
|
+
readonly kind: "string[]";
|
|
88
|
+
readonly required: true;
|
|
89
|
+
readonly minItems: 1;
|
|
90
|
+
readonly maxItems: 50;
|
|
91
|
+
readonly uniqueItems: true;
|
|
92
|
+
readonly item: {
|
|
93
|
+
readonly minLength: 3;
|
|
94
|
+
readonly maxLength: 254;
|
|
95
|
+
readonly pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$";
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
readonly cc: {
|
|
99
|
+
readonly kind: "string[]";
|
|
100
|
+
readonly required: false;
|
|
101
|
+
readonly minItems: 1;
|
|
102
|
+
readonly maxItems: 50;
|
|
103
|
+
readonly uniqueItems: true;
|
|
104
|
+
readonly item: {
|
|
105
|
+
readonly minLength: 3;
|
|
106
|
+
readonly maxLength: 254;
|
|
107
|
+
readonly pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$";
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
readonly bcc: {
|
|
111
|
+
readonly kind: "string[]";
|
|
112
|
+
readonly required: false;
|
|
113
|
+
readonly minItems: 1;
|
|
114
|
+
readonly maxItems: 50;
|
|
115
|
+
readonly uniqueItems: true;
|
|
116
|
+
readonly item: {
|
|
117
|
+
readonly minLength: 3;
|
|
118
|
+
readonly maxLength: 254;
|
|
119
|
+
readonly pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$";
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
readonly subject: {
|
|
123
|
+
readonly kind: "string";
|
|
124
|
+
readonly required: true;
|
|
125
|
+
readonly minLength: 1;
|
|
126
|
+
readonly maxLength: 256;
|
|
127
|
+
};
|
|
128
|
+
readonly text: {
|
|
129
|
+
readonly kind: "string";
|
|
130
|
+
readonly required: true;
|
|
131
|
+
readonly minLength: 1;
|
|
132
|
+
readonly maxLength: 4000;
|
|
133
|
+
};
|
|
134
|
+
readonly replyToMessageId: {
|
|
135
|
+
readonly kind: "string";
|
|
136
|
+
readonly required: false;
|
|
137
|
+
readonly minLength: 0;
|
|
138
|
+
readonly pattern: "^[A-Za-z0-9_-]{1,64}$";
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
readonly "warehouse.view.read": {
|
|
143
|
+
readonly idempotencyKey: false;
|
|
144
|
+
readonly fields: {
|
|
145
|
+
readonly columns: {
|
|
146
|
+
readonly kind: "string[]";
|
|
147
|
+
readonly required: true;
|
|
148
|
+
readonly minItems: 1;
|
|
149
|
+
readonly maxItems: 32;
|
|
150
|
+
readonly uniqueItems: false;
|
|
151
|
+
readonly item: {
|
|
152
|
+
readonly minLength: 0;
|
|
153
|
+
readonly pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$";
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
readonly limit: {
|
|
157
|
+
readonly kind: "integer";
|
|
158
|
+
readonly required: false;
|
|
159
|
+
readonly minimum: 1;
|
|
160
|
+
readonly maximum: 1000;
|
|
161
|
+
readonly default: 100;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
export type IntegrationExecuteOperation = keyof typeof IntegrationExecuteInputRules;
|
|
167
|
+
/** The upper bound of one integer input as a literal type: integrationExecuteInputMaximum('slack.channel.history', 'limit') is 15. */
|
|
168
|
+
export declare function integrationExecuteInputMaximum<O extends IntegrationExecuteOperation, F extends keyof (typeof IntegrationExecuteInputRules)[O]['fields']>(operation: O, field: F): Extract<(typeof IntegrationExecuteInputRules)[O]['fields'][F], {
|
|
169
|
+
maximum: number;
|
|
170
|
+
}>['maximum'];
|
|
171
|
+
export type IntegrationExecuteInputProblem = {
|
|
172
|
+
field: string;
|
|
173
|
+
message: string;
|
|
174
|
+
};
|
|
175
|
+
export type IntegrationExecuteInputValidation = {
|
|
176
|
+
ok: true;
|
|
177
|
+
input: Record<string, unknown>;
|
|
178
|
+
} | {
|
|
179
|
+
ok: false;
|
|
180
|
+
problem: IntegrationExecuteInputProblem;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Validates one operation's input against the contract and returns it with the
|
|
184
|
+
* contract's defaults applied. The message names the field and its allowed
|
|
185
|
+
* range; it is the sentence the platform answers with, so a caller may show it.
|
|
186
|
+
*/
|
|
187
|
+
export declare function validateIntegrationExecuteInput(operation: string, input: unknown): IntegrationExecuteInputValidation;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
|
|
3
|
+
export const IntegrationExecuteInputRules = {
|
|
4
|
+
"slack.channel.history": {
|
|
5
|
+
idempotencyKey: false,
|
|
6
|
+
fields: {
|
|
7
|
+
limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
"slack.message.post": {
|
|
11
|
+
idempotencyKey: true,
|
|
12
|
+
fields: {
|
|
13
|
+
text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"gmail.thread.list": {
|
|
17
|
+
idempotencyKey: false,
|
|
18
|
+
fields: {
|
|
19
|
+
limit: { kind: "integer", required: false, minimum: 1, maximum: 15, default: 15 },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
"gmail.message.read": {
|
|
23
|
+
idempotencyKey: false,
|
|
24
|
+
fields: {
|
|
25
|
+
messageId: { kind: "string", required: true, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"gmail.message.send": {
|
|
29
|
+
idempotencyKey: true,
|
|
30
|
+
fields: {
|
|
31
|
+
to: { kind: "string[]", required: true, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
|
|
32
|
+
cc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
|
|
33
|
+
bcc: { kind: "string[]", required: false, minItems: 1, maxItems: 50, uniqueItems: true, item: { minLength: 3, maxLength: 254, pattern: "^[^\\s@<>,;\"]+@[^\\s@<>,;\"]+\\.[^\\s@<>,;\"]+$" } },
|
|
34
|
+
subject: { kind: "string", required: true, minLength: 1, maxLength: 256 },
|
|
35
|
+
text: { kind: "string", required: true, minLength: 1, maxLength: 4000 },
|
|
36
|
+
replyToMessageId: { kind: "string", required: false, minLength: 0, pattern: "^[A-Za-z0-9_-]{1,64}$" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"warehouse.view.read": {
|
|
40
|
+
idempotencyKey: false,
|
|
41
|
+
fields: {
|
|
42
|
+
columns: { kind: "string[]", required: true, minItems: 1, maxItems: 32, uniqueItems: false, item: { minLength: 0, pattern: "^(?:\\*|[A-Za-z_][A-Za-z0-9_]{0,62})$" } },
|
|
43
|
+
limit: { kind: "integer", required: false, minimum: 1, maximum: 1000, default: 100 },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** The upper bound of one integer input as a literal type: integrationExecuteInputMaximum('slack.channel.history', 'limit') is 15. */
|
|
48
|
+
export function integrationExecuteInputMaximum(operation, field) {
|
|
49
|
+
const rule = IntegrationExecuteInputRules[operation].fields[field];
|
|
50
|
+
if (!rule || rule.kind !== 'integer')
|
|
51
|
+
throw new Error(`${String(operation)}.input.${String(field)} has no integer bound`);
|
|
52
|
+
return rule.maximum;
|
|
53
|
+
}
|
|
54
|
+
const patterns = new Map();
|
|
55
|
+
function matches(pattern, value) {
|
|
56
|
+
let compiled = patterns.get(pattern);
|
|
57
|
+
if (!compiled) {
|
|
58
|
+
compiled = new RegExp(pattern, 'u');
|
|
59
|
+
patterns.set(pattern, compiled);
|
|
60
|
+
}
|
|
61
|
+
return compiled.test(value);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validates one operation's input against the contract and returns it with the
|
|
65
|
+
* contract's defaults applied. The message names the field and its allowed
|
|
66
|
+
* range; it is the sentence the platform answers with, so a caller may show it.
|
|
67
|
+
*/
|
|
68
|
+
export function validateIntegrationExecuteInput(operation, input) {
|
|
69
|
+
const rule = IntegrationExecuteInputRules[operation];
|
|
70
|
+
if (!rule)
|
|
71
|
+
return { ok: false, problem: { field: 'operation', message: `${operation} is not a declared integration operation` } };
|
|
72
|
+
const raw = input === undefined ? {} : input;
|
|
73
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
74
|
+
return { ok: false, problem: { field: 'input', message: 'input must be an object' } };
|
|
75
|
+
const given = raw;
|
|
76
|
+
for (const name of Object.keys(given))
|
|
77
|
+
if (!Object.prototype.hasOwnProperty.call(rule.fields, name))
|
|
78
|
+
return { ok: false, problem: { field: name, message: `${name} is not an input of ${operation}` } };
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const [name, field] of Object.entries(rule.fields)) {
|
|
81
|
+
const value = given[name];
|
|
82
|
+
if (value === undefined || value === null) {
|
|
83
|
+
if (field.required)
|
|
84
|
+
return { ok: false, problem: { field: name, message: `${name} is required for ${operation}` } };
|
|
85
|
+
if (field.kind === 'integer' && field.default !== undefined)
|
|
86
|
+
out[name] = field.default;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (field.kind === 'integer') {
|
|
90
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < field.minimum || value > field.maximum)
|
|
91
|
+
return { ok: false, problem: { field: name, message: `${name} must be an integer between ${field.minimum} and ${field.maximum}` } };
|
|
92
|
+
}
|
|
93
|
+
else if (field.kind === 'string') {
|
|
94
|
+
if (typeof value !== 'string' || value.length < field.minLength || (field.maxLength !== undefined && value.length > field.maxLength))
|
|
95
|
+
return { ok: false, problem: { field: name, message: field.maxLength !== undefined ? `${name} must be ${field.minLength} to ${field.maxLength} characters` : `${name} must be a string of at least ${field.minLength} characters` } };
|
|
96
|
+
if (field.pattern && !matches(field.pattern, value))
|
|
97
|
+
return { ok: false, problem: { field: name, message: `${name} must match ${field.pattern}` } };
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!Array.isArray(value) || value.length < field.minItems || value.length > field.maxItems)
|
|
101
|
+
return { ok: false, problem: { field: name, message: `${name} must list ${field.minItems} to ${field.maxItems} items` } };
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const [index, item] of value.entries()) {
|
|
104
|
+
if (typeof item !== 'string' || item.length < field.item.minLength || (field.item.maxLength !== undefined && item.length > field.item.maxLength) || (field.item.pattern !== undefined && !matches(field.item.pattern, item)))
|
|
105
|
+
return { ok: false, problem: { field: name, message: `${name}[${index}] is not a valid ${name} entry` } };
|
|
106
|
+
if (field.uniqueItems && seen.has(item))
|
|
107
|
+
return { ok: false, problem: { field: name, message: `${name} must not repeat an entry` } };
|
|
108
|
+
seen.add(item);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
out[name] = value;
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, input: out };
|
|
114
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supabase-js compatibility layer for the Isomorph App SDK.
|
|
3
|
+
*
|
|
4
|
+
* Converted Supabase apps swap `createClient` from '@supabase/supabase-js' for
|
|
5
|
+
* `createSupabaseCompatClient(createClient())` and keep call sites unchanged.
|
|
6
|
+
* Everything here maps onto the existing Isomorph client surfaces only; nothing
|
|
7
|
+
* new is invented on the wire.
|
|
8
|
+
*
|
|
9
|
+
* Contract differences this bridges (all found in converted-app reviews):
|
|
10
|
+
* - supabase call sites destructure `{ data, error }` and expect NEVER to see a
|
|
11
|
+
* throw; the Isomorph SDK returns values and throws IsomorphError. Every async
|
|
12
|
+
* method here catches and returns a PostgrestError-shaped `error` instead.
|
|
13
|
+
* - `functions.invoke(name, { body })` must UNWRAP the `body` option before it
|
|
14
|
+
* reaches the action server (supabase-js does; a passthrough left payloads
|
|
15
|
+
* wrapped in `{ body: ... }`).
|
|
16
|
+
* - storage is bucket-scoped in supabase and path-scoped in Isomorph; object
|
|
17
|
+
* keys are mapped as `<bucket>/<path>` so distinct buckets stay distinct.
|
|
18
|
+
*/
|
|
19
|
+
import { type IsomorphResult, type IsomorphUser, type RealtimeSubscription } from './index.js';
|
|
20
|
+
/** PostgrestError-shaped error returned (never thrown) by every compat call. */
|
|
21
|
+
export interface SupabaseCompatError {
|
|
22
|
+
message: string;
|
|
23
|
+
/** Isomorph error category (AUTH_REQUIRED, FORBIDDEN, ...) when known. */
|
|
24
|
+
code?: string;
|
|
25
|
+
details?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export type SupabaseCompatResponse<Result> = {
|
|
28
|
+
data: Result;
|
|
29
|
+
error: null;
|
|
30
|
+
count: number | null;
|
|
31
|
+
} | {
|
|
32
|
+
data: null;
|
|
33
|
+
error: SupabaseCompatError;
|
|
34
|
+
count: null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Decode a selected result without asserting a relation shape the SDK cannot
|
|
38
|
+
* infer from a select string. The decoder should check the extra fields and
|
|
39
|
+
* return the desired shape; a thrown validation error becomes a compat error.
|
|
40
|
+
* Existing query errors bypass the decoder, and successful counts are retained.
|
|
41
|
+
*/
|
|
42
|
+
export declare function decodeSupabaseCompatResponse<T, Result>(response: SupabaseCompatResponse<T>, decode: (data: T) => Result): SupabaseCompatResponse<Result>;
|
|
43
|
+
/**
|
|
44
|
+
* The structural slice of the Isomorph QueryBuilder the compat layer relies on.
|
|
45
|
+
* The real QueryBuilder satisfies this; tests can hand in fakes.
|
|
46
|
+
*/
|
|
47
|
+
export interface CompatQuerySource<T, Result = T[]> {
|
|
48
|
+
select(columns?: string, options?: {
|
|
49
|
+
count?: 'exact';
|
|
50
|
+
head?: boolean;
|
|
51
|
+
}): CompatQuerySource<T, Result>;
|
|
52
|
+
insert(values: unknown): CompatQuerySource<T, Result>;
|
|
53
|
+
update(values: unknown): CompatQuerySource<T, Result>;
|
|
54
|
+
upsert(values: unknown, options?: {
|
|
55
|
+
onConflict?: string | string[];
|
|
56
|
+
}): CompatQuerySource<T, Result>;
|
|
57
|
+
delete(): CompatQuerySource<T, Result>;
|
|
58
|
+
eq(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
59
|
+
neq(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
60
|
+
gt(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
61
|
+
gte(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
62
|
+
lt(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
63
|
+
lte(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
64
|
+
is(column: string, value: unknown): CompatQuerySource<T, Result>;
|
|
65
|
+
not(column: string, operator: string, value: unknown): CompatQuerySource<T, Result>;
|
|
66
|
+
in(column: string, values: unknown[]): CompatQuerySource<T, Result>;
|
|
67
|
+
ilike(column: string, value: string): CompatQuerySource<T, Result>;
|
|
68
|
+
or(expression: string): CompatQuerySource<T, Result>;
|
|
69
|
+
order(column: string, options?: {
|
|
70
|
+
ascending?: boolean;
|
|
71
|
+
nullsFirst?: boolean;
|
|
72
|
+
}): CompatQuerySource<T, Result>;
|
|
73
|
+
limit(value: number): CompatQuerySource<T, Result>;
|
|
74
|
+
range(from: number, to: number): CompatQuerySource<T, Result>;
|
|
75
|
+
single(): CompatQuerySource<T, T>;
|
|
76
|
+
maybeSingle(): CompatQuerySource<T, T | null>;
|
|
77
|
+
execute(): Promise<IsomorphResult<Result>>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Thenable supabase-style builder. Awaiting it resolves
|
|
81
|
+
* `{ data, error, count }` and NEVER rejects: Isomorph throws are caught and
|
|
82
|
+
* converted to a PostgrestError-shaped `error` with `data: null`.
|
|
83
|
+
*/
|
|
84
|
+
type CompatQueryResult<Result, Head extends boolean> = Head extends true ? null : Result;
|
|
85
|
+
export declare class SupabaseCompatQueryBuilder<T, Result = T[], Head extends boolean = false> implements PromiseLike<SupabaseCompatResponse<CompatQueryResult<Result, Head>>> {
|
|
86
|
+
private readonly inner;
|
|
87
|
+
private readonly head;
|
|
88
|
+
constructor(inner: CompatQuerySource<T, Result>, ...head: [head: Head] | ([Head] extends [false] ? [] : never));
|
|
89
|
+
private chain;
|
|
90
|
+
select(columns: string | undefined, options: {
|
|
91
|
+
count?: 'exact';
|
|
92
|
+
head: true;
|
|
93
|
+
}): SupabaseCompatQueryBuilder<T, Result, true>;
|
|
94
|
+
select(columns?: string, options?: {
|
|
95
|
+
count?: 'exact';
|
|
96
|
+
head?: false;
|
|
97
|
+
}): SupabaseCompatQueryBuilder<T, Result, false>;
|
|
98
|
+
select(columns?: string, options?: {
|
|
99
|
+
count?: 'exact';
|
|
100
|
+
head?: boolean;
|
|
101
|
+
}): SupabaseCompatQueryBuilder<T, Result, boolean>;
|
|
102
|
+
insert(values: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
103
|
+
update(values: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
104
|
+
/** Optionless `upsert(row)` must pass through untouched (supabase allows it). */
|
|
105
|
+
upsert(values: unknown, options?: {
|
|
106
|
+
onConflict?: string | string[];
|
|
107
|
+
}): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
108
|
+
delete(): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
109
|
+
eq(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
110
|
+
neq(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
111
|
+
gt(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
112
|
+
gte(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
113
|
+
lt(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
114
|
+
lte(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
115
|
+
is(column: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
116
|
+
not(column: string, operator: string, value: unknown): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
117
|
+
in(column: string, values: unknown[]): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
118
|
+
ilike(column: string, value: string): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
119
|
+
or(expression: string): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
120
|
+
order(column: string, options?: {
|
|
121
|
+
ascending?: boolean;
|
|
122
|
+
nullsFirst?: boolean;
|
|
123
|
+
}): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
124
|
+
limit(value: number): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
125
|
+
range(from: number, to: number): SupabaseCompatQueryBuilder<T, Result, Head>;
|
|
126
|
+
single(): SupabaseCompatQueryBuilder<T, T, Head>;
|
|
127
|
+
maybeSingle(): SupabaseCompatQueryBuilder<T, T | null, Head>;
|
|
128
|
+
private resolve;
|
|
129
|
+
then<TResult1 = SupabaseCompatResponse<CompatQueryResult<Result, Head>>, TResult2 = never>(onfulfilled?: ((value: SupabaseCompatResponse<CompatQueryResult<Result, Head>>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
130
|
+
}
|
|
131
|
+
export interface SupabaseCompatUser {
|
|
132
|
+
id: string;
|
|
133
|
+
email: string;
|
|
134
|
+
app_metadata: Record<string, unknown>;
|
|
135
|
+
/** Isomorph's per-app user record (`IsomorphUser.appUser`) when it is an object. */
|
|
136
|
+
user_metadata: Record<string, unknown>;
|
|
137
|
+
}
|
|
138
|
+
export interface SupabaseCompatSession {
|
|
139
|
+
user: SupabaseCompatUser;
|
|
140
|
+
/**
|
|
141
|
+
* Isomorph authenticates with a gateway cookie; no bearer token exists. This
|
|
142
|
+
* placeholder keeps `if (session?.access_token)` truthiness checks working.
|
|
143
|
+
* It must never be forwarded as a credential.
|
|
144
|
+
*/
|
|
145
|
+
access_token: string;
|
|
146
|
+
token_type: 'bearer';
|
|
147
|
+
}
|
|
148
|
+
export type SupabaseCompatAuthEvent = 'SIGNED_IN' | 'SIGNED_OUT';
|
|
149
|
+
type CompatRealtimeStatus = 'SUBSCRIBED' | 'CLOSED' | 'CHANNEL_ERROR' | 'RECONNECTING' | 'REFETCH_REQUIRED';
|
|
150
|
+
/** Structural slice of Isomorph's RealtimeChannelBuilder (already supabase-shaped). */
|
|
151
|
+
export interface CompatChannel {
|
|
152
|
+
on(event: 'postgres_changes' | 'presence' | 'broadcast', filter: unknown, handler: (payload: unknown) => void): CompatChannel;
|
|
153
|
+
subscribe(callback?: (status: CompatRealtimeStatus) => void): RealtimeSubscription;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The slice of the Isomorph client the compat layer consumes. The object
|
|
157
|
+
* returned by the SDK's `createClient` satisfies it; tests can hand in fakes.
|
|
158
|
+
*/
|
|
159
|
+
export interface IsomorphCompatSource {
|
|
160
|
+
identity: {
|
|
161
|
+
current(): Promise<IsomorphUser | null>;
|
|
162
|
+
signOut(returnTo?: string): string;
|
|
163
|
+
onChange(listener: (user: IsomorphUser | null) => void): () => void;
|
|
164
|
+
};
|
|
165
|
+
data: {
|
|
166
|
+
from<T = Record<string, unknown>>(table: string): CompatQuerySource<T>;
|
|
167
|
+
rpc<T = unknown>(name: string, args?: Record<string, unknown>): Promise<T>;
|
|
168
|
+
};
|
|
169
|
+
files: {
|
|
170
|
+
upload(path: string, body: Blob, checksum?: string): Promise<unknown>;
|
|
171
|
+
list(prefix?: string): Promise<unknown[]>;
|
|
172
|
+
remove(paths: string[]): Promise<void>;
|
|
173
|
+
getPublicUrl(path: string): string;
|
|
174
|
+
createSignedUrl(path: string, expiresIn?: number): Promise<{
|
|
175
|
+
signedUrl: string;
|
|
176
|
+
}>;
|
|
177
|
+
createSignedUrls(paths: string[], expiresIn?: number): Promise<Array<{
|
|
178
|
+
path: string;
|
|
179
|
+
signedUrl: string;
|
|
180
|
+
}>>;
|
|
181
|
+
};
|
|
182
|
+
realtime: {
|
|
183
|
+
channel(name: string): CompatChannel;
|
|
184
|
+
};
|
|
185
|
+
actions: {
|
|
186
|
+
invoke<T = unknown>(name: string, input: unknown): Promise<T>;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export interface SupabaseCompatFunctionsInvokeOptions {
|
|
190
|
+
/** Unwrapped and sent as the action input, exactly as supabase-js does. */
|
|
191
|
+
body?: unknown;
|
|
192
|
+
/** Nonempty custom headers return an error; the Isomorph gateway owns headers. */
|
|
193
|
+
headers?: Record<string, string>;
|
|
194
|
+
/** Non-POST methods return an error; Isomorph actions are always POST. */
|
|
195
|
+
method?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Row typing from a supabase-js generated `Database` type. Pass it exactly as
|
|
199
|
+
* the source passed it to `createClient<Database>`:
|
|
200
|
+
* `createSupabaseCompatClient<Database>(isomorph)`. Without it every row is
|
|
201
|
+
* `Record<string, unknown>`, and an application that relied on the generated
|
|
202
|
+
* types loses them at every access (ERP: `Type 'unknown' is not assignable`
|
|
203
|
+
* across its components after conversion).
|
|
204
|
+
*/
|
|
205
|
+
type CompatTables<Database> = Database extends {
|
|
206
|
+
public: {
|
|
207
|
+
Tables: infer T;
|
|
208
|
+
};
|
|
209
|
+
} ? T : never;
|
|
210
|
+
export type CompatRow<Database, Name extends string> = [CompatTables<Database>] extends [never] ? Record<string, unknown> : Name extends keyof CompatTables<Database> ? CompatTables<Database>[Name] extends {
|
|
211
|
+
Row: infer R;
|
|
212
|
+
} ? R : Record<string, unknown> : Record<string, unknown>;
|
|
213
|
+
/**
|
|
214
|
+
* Build a supabase-js-shaped client on top of an existing Isomorph client.
|
|
215
|
+
* Pass the object returned by the SDK's `createClient`.
|
|
216
|
+
*/
|
|
217
|
+
export declare function createSupabaseCompatClient<Database = never>(isomorph: IsomorphCompatSource): {
|
|
218
|
+
from<Name extends string>(table: Name): SupabaseCompatQueryBuilder<CompatRow<Database, Name>, CompatRow<Database, Name>[]>;
|
|
219
|
+
rpc: <T = unknown>(name: string, params?: Record<string, unknown>) => Promise<{
|
|
220
|
+
data: T | null;
|
|
221
|
+
error: SupabaseCompatError | null;
|
|
222
|
+
}>;
|
|
223
|
+
auth: {
|
|
224
|
+
getSession: () => Promise<{
|
|
225
|
+
data: {
|
|
226
|
+
session: SupabaseCompatSession | null;
|
|
227
|
+
};
|
|
228
|
+
error: SupabaseCompatError | null;
|
|
229
|
+
}>;
|
|
230
|
+
getUser: () => Promise<{
|
|
231
|
+
data: {
|
|
232
|
+
user: SupabaseCompatUser | null;
|
|
233
|
+
};
|
|
234
|
+
error: SupabaseCompatError | null;
|
|
235
|
+
}>;
|
|
236
|
+
onAuthStateChange: (callback: (event: SupabaseCompatAuthEvent, session: SupabaseCompatSession | null) => void) => {
|
|
237
|
+
data: {
|
|
238
|
+
subscription: {
|
|
239
|
+
unsubscribe: () => void;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* The Isomorph edge gateway owns sign-out: this clears local identity and
|
|
245
|
+
* redirects the browser through /__harbour/logout (via identity.signOut).
|
|
246
|
+
*/
|
|
247
|
+
signOut: () => Promise<{
|
|
248
|
+
error: null;
|
|
249
|
+
}>;
|
|
250
|
+
};
|
|
251
|
+
functions: {
|
|
252
|
+
invoke: <T = unknown>(name: string, options?: SupabaseCompatFunctionsInvokeOptions) => Promise<{
|
|
253
|
+
data: T | null;
|
|
254
|
+
error: SupabaseCompatError | null;
|
|
255
|
+
}>;
|
|
256
|
+
};
|
|
257
|
+
storage: {
|
|
258
|
+
from: (bucket: string) => {
|
|
259
|
+
upload: (path: string, body: Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer> | string, options?: {
|
|
260
|
+
contentType?: string;
|
|
261
|
+
upsert?: boolean;
|
|
262
|
+
cacheControl?: string;
|
|
263
|
+
}) => Promise<{
|
|
264
|
+
data: {
|
|
265
|
+
path: string;
|
|
266
|
+
fullPath: string;
|
|
267
|
+
} | null;
|
|
268
|
+
error: SupabaseCompatError | null;
|
|
269
|
+
}>;
|
|
270
|
+
/** Mapped through createSignedUrl + a plain fetch of the signed URL. */
|
|
271
|
+
download: (path: string) => Promise<{
|
|
272
|
+
data: Blob | null;
|
|
273
|
+
error: SupabaseCompatError | null;
|
|
274
|
+
}>;
|
|
275
|
+
remove: (paths: string[]) => Promise<{
|
|
276
|
+
data: Array<{
|
|
277
|
+
name: string;
|
|
278
|
+
}> | null;
|
|
279
|
+
error: SupabaseCompatError | null;
|
|
280
|
+
}>;
|
|
281
|
+
list: (prefix?: string) => Promise<{
|
|
282
|
+
data: unknown[] | null;
|
|
283
|
+
error: SupabaseCompatError | null;
|
|
284
|
+
}>;
|
|
285
|
+
/** Sync, like supabase-js: URL construction only, no network call. */
|
|
286
|
+
getPublicUrl: (path: string) => {
|
|
287
|
+
data: {
|
|
288
|
+
publicUrl: string;
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
createSignedUrl: (path: string, expiresIn?: number) => Promise<{
|
|
292
|
+
data: {
|
|
293
|
+
signedUrl: string;
|
|
294
|
+
} | null;
|
|
295
|
+
error: SupabaseCompatError | null;
|
|
296
|
+
}>;
|
|
297
|
+
createSignedUrls: (paths: string[], expiresIn?: number) => Promise<{
|
|
298
|
+
data: Array<{
|
|
299
|
+
path: string;
|
|
300
|
+
signedUrl: string;
|
|
301
|
+
}> | null;
|
|
302
|
+
error: SupabaseCompatError | null;
|
|
303
|
+
}>;
|
|
304
|
+
/** No Isomorph files move; declared so call sites fail loudly, not with a TypeError. */
|
|
305
|
+
move: (fromPath: string, toPath: string) => Promise<{
|
|
306
|
+
data: null;
|
|
307
|
+
error: SupabaseCompatError;
|
|
308
|
+
}>;
|
|
309
|
+
/** No Isomorph files copy; declared so call sites fail loudly, not with a TypeError. */
|
|
310
|
+
copy: (fromPath: string, toPath: string) => Promise<{
|
|
311
|
+
data: null;
|
|
312
|
+
error: SupabaseCompatError;
|
|
313
|
+
}>;
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
/** Isomorph's channel builder is already supabase-shaped; pure passthrough. */
|
|
317
|
+
channel: (name: string) => CompatChannel;
|
|
318
|
+
removeChannel: (subscription: RealtimeSubscription) => Promise<"ok" | "error">;
|
|
319
|
+
};
|
|
320
|
+
export type SupabaseCompatClient = ReturnType<typeof createSupabaseCompatClient>;
|
|
321
|
+
type Extends<A extends B, B> = A;
|
|
322
|
+
type IsomorphClientSatisfiesCompatSource = Extends<import('./index.js').IsomorphClient, IsomorphCompatSource>;
|
|
323
|
+
export type { IsomorphClientSatisfiesCompatSource as _IsomorphClientSatisfiesCompatSource };
|