@ankhorage/supabase-db 1.0.2 → 1.0.4
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/CHANGELOG.md +13 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +69 -63
- package/dist/adapter.js.map +1 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +34 -55
- package/dist/realtime.js.map +1 -1
- package/package.json +9 -11
- package/src/adapter.test.ts +108 -110
- package/src/adapter.ts +87 -109
- package/src/admin.test.ts +87 -91
- package/src/realtime.test.ts +102 -106
- package/src/realtime.ts +57 -74
package/src/adapter.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import { createSupabaseDbAdapter } from './adapter.js';
|
|
4
4
|
|
|
@@ -12,139 +12,137 @@ interface PostRecord {
|
|
|
12
12
|
readonly title: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const result = await adapter.select<PostRecord>({
|
|
28
|
-
table: 'posts',
|
|
29
|
-
columns: ['id', 'title'],
|
|
30
|
-
filters: [{ field: 'title', operator: 'startsWith', value: 'Hel' }],
|
|
31
|
-
sort: [{ field: 'title', direction: 'desc' }],
|
|
32
|
-
page: { limit: 10, offset: 5 },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
expect(result).toEqual({ ok: true, data: [{ id: 'post-1', title: 'Hello' }] });
|
|
36
|
-
expect(calls).toHaveLength(1);
|
|
37
|
-
|
|
38
|
-
const url = new URL(calls[0]?.url ?? 'https://invalid.test');
|
|
39
|
-
expect(url.pathname).toBe('/rest/v1/posts');
|
|
40
|
-
expect(url.searchParams.get('select')).toBe('id,title');
|
|
41
|
-
expect(url.searchParams.get('title')).toBe('like.Hel*');
|
|
42
|
-
expect(url.searchParams.get('order')).toBe('title.desc');
|
|
43
|
-
expect(url.searchParams.get('limit')).toBe('10');
|
|
44
|
-
expect(url.searchParams.get('offset')).toBe('5');
|
|
45
|
-
expect(readHeader(calls[0]?.init, 'apikey')).toBe('anon');
|
|
46
|
-
expect(readHeader(calls[0]?.init, 'Prefer')).toBe('return=representation');
|
|
15
|
+
test('maps select input to Supabase PostgREST query params', async () => {
|
|
16
|
+
const calls: FetchCall[] = [];
|
|
17
|
+
const adapter = createSupabaseDbAdapter({
|
|
18
|
+
url: 'https://example.supabase.co',
|
|
19
|
+
anonKey: 'anon',
|
|
20
|
+
fetch: (input, init) => {
|
|
21
|
+
calls.push({ url: fetchInputToString(input), init });
|
|
22
|
+
return Promise.resolve(jsonResponse([{ id: 'post-1', title: 'Hello' }]));
|
|
23
|
+
},
|
|
47
24
|
});
|
|
48
25
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
26
|
+
const result = await adapter.select<PostRecord>({
|
|
27
|
+
table: 'posts',
|
|
28
|
+
columns: ['id', 'title'],
|
|
29
|
+
filters: [{ field: 'title', operator: 'startsWith', value: 'Hel' }],
|
|
30
|
+
sort: [{ field: 'title', direction: 'desc' }],
|
|
31
|
+
page: { limit: 10, offset: 5 },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({ ok: true, data: [{ id: 'post-1', title: 'Hello' }] });
|
|
35
|
+
expect(calls).toHaveLength(1);
|
|
36
|
+
|
|
37
|
+
const url = new URL(calls[0]?.url ?? 'https://invalid.test');
|
|
38
|
+
expect(url.pathname).toBe('/rest/v1/posts');
|
|
39
|
+
expect(url.searchParams.get('select')).toBe('id,title');
|
|
40
|
+
expect(url.searchParams.get('title')).toBe('like.Hel*');
|
|
41
|
+
expect(url.searchParams.get('order')).toBe('title.desc');
|
|
42
|
+
expect(url.searchParams.get('limit')).toBe('10');
|
|
43
|
+
expect(url.searchParams.get('offset')).toBe('5');
|
|
44
|
+
expect(readHeader(calls[0]?.init, 'apikey')).toBe('anon');
|
|
45
|
+
expect(readHeader(calls[0]?.init, 'Prefer')).toBe('return=representation');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('finds records by id', async () => {
|
|
49
|
+
const adapter = createSupabaseDbAdapter({
|
|
50
|
+
url: 'https://example.supabase.co',
|
|
51
|
+
anonKey: 'anon',
|
|
52
|
+
fetch: () => Promise.resolve(jsonResponse([{ id: 'post-1', title: 'Hello' }])),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = await adapter.findById<PostRecord>({ table: 'posts', id: 'post-1' });
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
expect(result).toEqual({ ok: true, data: { id: 'post-1', title: 'Hello' } });
|
|
58
|
+
});
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
test('normalizes empty findById result to null', async () => {
|
|
61
|
+
const adapter = createSupabaseDbAdapter({
|
|
62
|
+
url: 'https://example.supabase.co',
|
|
63
|
+
anonKey: 'anon',
|
|
64
|
+
fetch: () => Promise.resolve(jsonResponse([])),
|
|
59
65
|
});
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
const adapter = createSupabaseDbAdapter({
|
|
63
|
-
url: 'https://example.supabase.co',
|
|
64
|
-
anonKey: 'anon',
|
|
65
|
-
fetch: () => Promise.resolve(jsonResponse([])),
|
|
66
|
-
});
|
|
67
|
+
const result = await adapter.findById<PostRecord>({ table: 'posts', id: 'missing' });
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
expect(result).toEqual({ ok: true, data: null });
|
|
70
|
+
});
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
test('maps insert values to POST with returning representation', async () => {
|
|
73
|
+
const calls: FetchCall[] = [];
|
|
74
|
+
const adapter = createSupabaseDbAdapter({
|
|
75
|
+
url: 'https://example.supabase.co',
|
|
76
|
+
anonKey: 'anon',
|
|
77
|
+
fetch: (input, init) => {
|
|
78
|
+
calls.push({ url: fetchInputToString(input), init });
|
|
79
|
+
return Promise.resolve(jsonResponse([{ id: 'post-1', title: 'Hello' }]));
|
|
80
|
+
},
|
|
71
81
|
});
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
url: 'https://example.supabase.co',
|
|
77
|
-
anonKey: 'anon',
|
|
78
|
-
fetch: (input, init) => {
|
|
79
|
-
calls.push({ url: fetchInputToString(input), init });
|
|
80
|
-
return Promise.resolve(jsonResponse([{ id: 'post-1', title: 'Hello' }]));
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const result = await adapter.insert<PostRecord>({
|
|
85
|
-
table: 'posts',
|
|
86
|
-
values: { id: 'post-1', title: 'Hello' },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(result.ok).toBe(true);
|
|
90
|
-
expect(calls[0]?.init?.method).toBe('POST');
|
|
91
|
-
expect(calls[0]?.init?.body).toBe(JSON.stringify({ id: 'post-1', title: 'Hello' }));
|
|
83
|
+
const result = await adapter.insert<PostRecord>({
|
|
84
|
+
table: 'posts',
|
|
85
|
+
values: { id: 'post-1', title: 'Hello' },
|
|
92
86
|
});
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
filters: [],
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(result.ok).toBe(false);
|
|
108
|
-
expect(result.ok === false ? result.error.code : '').toBe('validation_error');
|
|
88
|
+
expect(result.ok).toBe(true);
|
|
89
|
+
expect(calls[0]?.init?.method).toBe('POST');
|
|
90
|
+
expect(calls[0]?.init?.body).toBe(JSON.stringify({ id: 'post-1', title: 'Hello' }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('requires filters for update', async () => {
|
|
94
|
+
const adapter = createSupabaseDbAdapter({
|
|
95
|
+
url: 'https://example.supabase.co',
|
|
96
|
+
anonKey: 'anon',
|
|
97
|
+
fetch: () => Promise.resolve(jsonResponse([])),
|
|
109
98
|
});
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
100
|
+
const result = await adapter.update<PostRecord>({
|
|
101
|
+
table: 'posts',
|
|
102
|
+
values: { title: 'Updated' },
|
|
103
|
+
filters: [],
|
|
104
|
+
});
|
|
117
105
|
|
|
118
|
-
|
|
106
|
+
expect(result.ok).toBe(false);
|
|
107
|
+
expect(result.ok === false ? result.error.code : '').toBe('validation_error');
|
|
108
|
+
});
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
test('requires filters for delete', async () => {
|
|
111
|
+
const adapter = createSupabaseDbAdapter({
|
|
112
|
+
url: 'https://example.supabase.co',
|
|
113
|
+
anonKey: 'anon',
|
|
114
|
+
fetch: () => Promise.resolve(jsonResponse([])),
|
|
122
115
|
});
|
|
123
116
|
|
|
124
|
-
|
|
125
|
-
const adapter = createSupabaseDbAdapter({
|
|
126
|
-
url: 'https://example.supabase.co',
|
|
127
|
-
anonKey: 'anon',
|
|
128
|
-
fetch: () => Promise.resolve(jsonResponse({ message: 'permission denied' }, 403)),
|
|
129
|
-
});
|
|
117
|
+
const result = await adapter.delete<PostRecord>({ table: 'posts', filters: [] });
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
expect(result.ok === false ? result.error.code : '').toBe('validation_error');
|
|
121
|
+
});
|
|
132
122
|
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
test('normalizes provider errors', async () => {
|
|
124
|
+
const adapter = createSupabaseDbAdapter({
|
|
125
|
+
url: 'https://example.supabase.co',
|
|
126
|
+
anonKey: 'anon',
|
|
127
|
+
fetch: () => Promise.resolve(jsonResponse({ message: 'permission denied' }, 403)),
|
|
135
128
|
});
|
|
136
129
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
fetch: () => Promise.resolve(jsonResponse([])),
|
|
143
|
-
});
|
|
130
|
+
const result = await adapter.select<PostRecord>({ table: 'posts' });
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(false);
|
|
133
|
+
expect(result.ok === false ? result.error.code : '').toBe('permission_denied');
|
|
134
|
+
});
|
|
144
135
|
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
test('exposes realtime capability only when configured with a client', () => {
|
|
137
|
+
const adapter = createSupabaseDbAdapter({
|
|
138
|
+
url: 'https://example.supabase.co',
|
|
139
|
+
anonKey: 'anon',
|
|
140
|
+
realtime: false,
|
|
141
|
+
fetch: () => Promise.resolve(jsonResponse([])),
|
|
147
142
|
});
|
|
143
|
+
|
|
144
|
+
expect(adapter.capabilities.realtime).toBe(false);
|
|
145
|
+
expect('realtime' in adapter).toBe(false);
|
|
148
146
|
});
|
|
149
147
|
|
|
150
148
|
function jsonResponse(body: unknown, status = 200): Response {
|
package/src/adapter.ts
CHANGED
|
@@ -33,113 +33,91 @@ interface DbFindByIdSuccessResult<TRecord extends object> {
|
|
|
33
33
|
|
|
34
34
|
export function createSupabaseDbAdapter(options: SupabaseDbAdapterOptions): SupabaseDbAdapter {
|
|
35
35
|
const config = normalizeConfig(options);
|
|
36
|
-
const baseAdapter
|
|
36
|
+
const baseAdapter = createBaseAdapter(config);
|
|
37
|
+
if (!config.realtime || config.realtimeClient === undefined) return baseAdapter;
|
|
38
|
+
return {
|
|
39
|
+
...baseAdapter,
|
|
40
|
+
realtime: createRealtimeApi({
|
|
41
|
+
client: config.realtimeClient,
|
|
42
|
+
defaultSchema: config.schema,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createBaseAdapter(config: NormalizedConfig): DbAdapter {
|
|
48
|
+
return {
|
|
37
49
|
capabilities: {
|
|
38
50
|
transactions: false,
|
|
39
51
|
returning: true,
|
|
40
52
|
realtime: config.realtime && config.realtimeClient !== undefined,
|
|
41
53
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
input: DbSelectInput,
|
|
45
|
-
): Promise<DbResult<TRecord[]>> {
|
|
46
|
-
return requestRows<TRecord>(config, buildSelectUrl(config.url, input), {
|
|
47
|
-
method: 'GET',
|
|
48
|
-
});
|
|
54
|
+
select<TRecord extends object = DbRecord>(input: DbSelectInput) {
|
|
55
|
+
return selectRows<TRecord>(config, input);
|
|
49
56
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
input: DbFindByIdInput,
|
|
53
|
-
): Promise<DbResult<TRecord | null>> {
|
|
54
|
-
const result = await requestRows<TRecord>(
|
|
55
|
-
config,
|
|
56
|
-
buildSelectUrl(config.url, {
|
|
57
|
-
table: input.table,
|
|
58
|
-
columns: input.columns,
|
|
59
|
-
filters: [{ field: input.idField ?? 'id', operator: 'eq', value: input.id }],
|
|
60
|
-
page: { limit: 1 },
|
|
61
|
-
schema: input.schema,
|
|
62
|
-
}),
|
|
63
|
-
{ method: 'GET' },
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
if (!result.ok) {
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const success: DbFindByIdSuccessResult<TRecord> = {
|
|
71
|
-
ok: true,
|
|
72
|
-
data: result.data[0] ?? null,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return success;
|
|
57
|
+
findById<TRecord extends object = DbRecord>(input: DbFindByIdInput) {
|
|
58
|
+
return findRowById<TRecord>(config, input);
|
|
76
59
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
input: DbInsertInput<TRecord>,
|
|
80
|
-
): Promise<DbResult<TRecord[]>> {
|
|
81
|
-
const url = buildMutationUrl(config.url, input.table, []);
|
|
82
|
-
|
|
83
|
-
return requestRows<TRecord>(config, url, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
body: JSON.stringify(input.values),
|
|
86
|
-
});
|
|
60
|
+
insert<TRecord extends object = DbRecord>(input: DbInsertInput<TRecord>) {
|
|
61
|
+
return insertRows<TRecord>(config, input);
|
|
87
62
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
input: DbUpdateInput<TRecord>,
|
|
91
|
-
): Promise<DbResult<TRecord[]>> {
|
|
92
|
-
const validationError = validateRequiredFilters(input.filters, 'Update');
|
|
93
|
-
|
|
94
|
-
if (validationError !== null) {
|
|
95
|
-
return validationError;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const url = buildMutationUrl(config.url, input.table, input.filters);
|
|
99
|
-
|
|
100
|
-
return requestRows<TRecord>(config, url, {
|
|
101
|
-
method: 'PATCH',
|
|
102
|
-
body: JSON.stringify(input.values),
|
|
103
|
-
});
|
|
63
|
+
update<TRecord extends object = DbRecord>(input: DbUpdateInput<TRecord>) {
|
|
64
|
+
return updateRows<TRecord>(config, input);
|
|
104
65
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
input: DbDeleteInput,
|
|
108
|
-
): Promise<DbResult<TRecord[]>> {
|
|
109
|
-
const validationError = validateRequiredFilters(input.filters, 'Delete');
|
|
110
|
-
|
|
111
|
-
if (validationError !== null) {
|
|
112
|
-
return validationError;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const url = buildMutationUrl(config.url, input.table, input.filters);
|
|
116
|
-
|
|
117
|
-
return requestRows<TRecord>(config, url, {
|
|
118
|
-
method: 'DELETE',
|
|
119
|
-
});
|
|
66
|
+
delete<TRecord extends object = DbRecord>(input: DbDeleteInput) {
|
|
67
|
+
return deleteRows<TRecord>(config, input);
|
|
120
68
|
},
|
|
121
69
|
};
|
|
70
|
+
}
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
72
|
+
function deleteRows<TRecord extends object>(
|
|
73
|
+
config: NormalizedConfig,
|
|
74
|
+
input: DbDeleteInput,
|
|
75
|
+
): Promise<DbResult<TRecord[]>> {
|
|
76
|
+
const validationError = validateRequiredFilters(input.filters, 'Delete');
|
|
77
|
+
if (validationError !== null) return Promise.resolve(validationError);
|
|
78
|
+
const url = buildMutationUrl(config.url, input.table, input.filters);
|
|
79
|
+
return requestRows<TRecord>(config, url, { method: 'DELETE' });
|
|
80
|
+
}
|
|
126
81
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
82
|
+
async function findRowById<TRecord extends object>(
|
|
83
|
+
config: NormalizedConfig,
|
|
84
|
+
input: DbFindByIdInput,
|
|
85
|
+
): Promise<DbResult<TRecord | null>> {
|
|
86
|
+
const result = await requestRows<TRecord>(
|
|
87
|
+
config,
|
|
88
|
+
buildSelectUrl(config.url, {
|
|
89
|
+
table: input.table,
|
|
90
|
+
columns: input.columns,
|
|
91
|
+
filters: [{ field: input.idField ?? 'id', operator: 'eq', value: input.id }],
|
|
92
|
+
page: { limit: 1 },
|
|
93
|
+
schema: input.schema,
|
|
132
94
|
}),
|
|
95
|
+
{ method: 'GET' },
|
|
96
|
+
);
|
|
97
|
+
if (!result.ok) return result;
|
|
98
|
+
const success: DbFindByIdSuccessResult<TRecord> = {
|
|
99
|
+
ok: true,
|
|
100
|
+
data: result.data[0] ?? null,
|
|
133
101
|
};
|
|
102
|
+
return success;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function insertRows<TRecord extends object>(
|
|
106
|
+
config: NormalizedConfig,
|
|
107
|
+
input: DbInsertInput<TRecord>,
|
|
108
|
+
): Promise<DbResult<TRecord[]>> {
|
|
109
|
+
const url = buildMutationUrl(config.url, input.table, []);
|
|
110
|
+
return requestRows<TRecord>(config, url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
body: JSON.stringify(input.values),
|
|
113
|
+
});
|
|
134
114
|
}
|
|
135
115
|
|
|
136
116
|
function normalizeConfig(options: SupabaseDbAdapterOptions): NormalizedConfig {
|
|
137
117
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
138
|
-
|
|
139
118
|
if (typeof fetchImplementation !== 'function') {
|
|
140
119
|
throw new TypeError('A fetch implementation is required to use Supabase Database.');
|
|
141
120
|
}
|
|
142
|
-
|
|
143
121
|
return {
|
|
144
122
|
url: validateUrl(options.url),
|
|
145
123
|
anonKey: validateKey(options.anonKey, 'Supabase anon key'),
|
|
@@ -161,13 +139,8 @@ async function requestRows<TRecord extends object>(
|
|
|
161
139
|
headers: createHeaders(config, init.headers),
|
|
162
140
|
});
|
|
163
141
|
const body = await readJsonBody(response);
|
|
164
|
-
|
|
165
|
-
if (!response.ok) {
|
|
166
|
-
return { ok: false, error: mapHttpError(response.status, body) };
|
|
167
|
-
}
|
|
168
|
-
|
|
142
|
+
if (!response.ok) return { ok: false, error: mapHttpError(response.status, body) };
|
|
169
143
|
const records = normalizeRecords<TRecord>(body);
|
|
170
|
-
|
|
171
144
|
if (records === null) {
|
|
172
145
|
return {
|
|
173
146
|
ok: false,
|
|
@@ -178,17 +151,35 @@ async function requestRows<TRecord extends object>(
|
|
|
178
151
|
),
|
|
179
152
|
};
|
|
180
153
|
}
|
|
181
|
-
|
|
182
154
|
return { ok: true, data: records };
|
|
183
155
|
} catch (error) {
|
|
184
156
|
if (error instanceof TypeError) {
|
|
185
157
|
return { ok: false, error: createDbError('validation_error', error.message, error) };
|
|
186
158
|
}
|
|
187
|
-
|
|
188
159
|
return { ok: false, error: mapNetworkError(error) };
|
|
189
160
|
}
|
|
190
161
|
}
|
|
191
162
|
|
|
163
|
+
function selectRows<TRecord extends object>(
|
|
164
|
+
config: NormalizedConfig,
|
|
165
|
+
input: DbSelectInput,
|
|
166
|
+
): Promise<DbResult<TRecord[]>> {
|
|
167
|
+
return requestRows<TRecord>(config, buildSelectUrl(config.url, input), { method: 'GET' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function updateRows<TRecord extends object>(
|
|
171
|
+
config: NormalizedConfig,
|
|
172
|
+
input: DbUpdateInput<TRecord>,
|
|
173
|
+
): Promise<DbResult<TRecord[]>> {
|
|
174
|
+
const validationError = validateRequiredFilters(input.filters, 'Update');
|
|
175
|
+
if (validationError !== null) return Promise.resolve(validationError);
|
|
176
|
+
const url = buildMutationUrl(config.url, input.table, input.filters);
|
|
177
|
+
return requestRows<TRecord>(config, url, {
|
|
178
|
+
method: 'PATCH',
|
|
179
|
+
body: JSON.stringify(input.values),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
192
183
|
function validateRequiredFilters(
|
|
193
184
|
filters: readonly DbFilter[],
|
|
194
185
|
label: string,
|
|
@@ -213,28 +204,21 @@ function createHeaders(
|
|
|
213
204
|
existingHeaders: HeadersInit | undefined,
|
|
214
205
|
): Headers {
|
|
215
206
|
const headers = new Headers(existingHeaders);
|
|
216
|
-
|
|
217
207
|
headers.set('apikey', config.anonKey);
|
|
218
208
|
headers.set('Authorization', `Bearer ${config.anonKey}`);
|
|
219
209
|
headers.set('Accept', 'application/json');
|
|
220
210
|
headers.set('Content-Type', 'application/json');
|
|
221
211
|
headers.set('Prefer', 'return=representation');
|
|
222
|
-
|
|
223
212
|
if (config.schema !== 'public') {
|
|
224
213
|
headers.set('Accept-Profile', config.schema);
|
|
225
214
|
headers.set('Content-Profile', config.schema);
|
|
226
215
|
}
|
|
227
|
-
|
|
228
216
|
return headers;
|
|
229
217
|
}
|
|
230
218
|
|
|
231
219
|
async function readJsonBody(response: Response): Promise<unknown> {
|
|
232
220
|
const text = await response.text();
|
|
233
|
-
|
|
234
|
-
if (text.trim().length === 0) {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
|
|
221
|
+
if (text.trim().length === 0) return [];
|
|
238
222
|
try {
|
|
239
223
|
return JSON.parse(text);
|
|
240
224
|
} catch {
|
|
@@ -243,14 +227,8 @@ async function readJsonBody(response: Response): Promise<unknown> {
|
|
|
243
227
|
}
|
|
244
228
|
|
|
245
229
|
function normalizeRecords<TRecord extends object>(value: unknown): TRecord[] | null {
|
|
246
|
-
if (Array.isArray(value))
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (isRecord<TRecord>(value)) {
|
|
251
|
-
return [value];
|
|
252
|
-
}
|
|
253
|
-
|
|
230
|
+
if (Array.isArray(value)) return value.filter(isRecord<TRecord>);
|
|
231
|
+
if (isRecord<TRecord>(value)) return [value];
|
|
254
232
|
return null;
|
|
255
233
|
}
|
|
256
234
|
|