@svadmin/directus 0.9.7 → 0.9.10
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/package.json +3 -3
- package/src/data-provider.ts +273 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/directus",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"description": "Directus DataProvider for svadmin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.
|
|
23
|
+
"@svadmin/core": "^0.41.1"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/
|
|
28
|
+
"url": "https://github.com/vibeunion/svadmin.git",
|
|
29
29
|
"directory": "packages/directus"
|
|
30
30
|
},
|
|
31
31
|
"peerDependenciesMeta": {
|
package/src/data-provider.ts
CHANGED
|
@@ -1,84 +1,310 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import type {
|
|
3
|
-
|
|
4
|
-
CreateParams,
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
BaseRecord,
|
|
3
|
+
CreateParams,
|
|
4
|
+
CreateResult,
|
|
5
|
+
CrudOperator,
|
|
6
|
+
CustomParams,
|
|
7
|
+
CustomResult,
|
|
8
|
+
DataProvider,
|
|
9
|
+
DeleteParams,
|
|
10
|
+
DeleteResult,
|
|
11
|
+
FieldFilter,
|
|
12
|
+
Filter,
|
|
13
|
+
GetListParams,
|
|
14
|
+
GetListResult,
|
|
15
|
+
GetManyParams,
|
|
16
|
+
GetManyResult,
|
|
17
|
+
GetOneParams,
|
|
18
|
+
GetOneResult,
|
|
19
|
+
Sort,
|
|
20
|
+
UpdateParams,
|
|
21
|
+
UpdateResult,
|
|
7
22
|
} from '@svadmin/core';
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
const DIRECTUS_OPERATOR_MAP: Record<CrudOperator, string> = {
|
|
25
|
+
eq: '_eq',
|
|
26
|
+
ne: '_neq',
|
|
27
|
+
lt: '_lt',
|
|
28
|
+
gt: '_gt',
|
|
29
|
+
lte: '_lte',
|
|
30
|
+
gte: '_gte',
|
|
31
|
+
contains: '_contains',
|
|
32
|
+
ncontains: '_ncontains',
|
|
33
|
+
startswith: '_starts_with',
|
|
34
|
+
endswith: '_ends_with',
|
|
35
|
+
in: '_in',
|
|
36
|
+
nin: '_nin',
|
|
37
|
+
null: '_null',
|
|
38
|
+
nnull: '_nnull',
|
|
39
|
+
between: '_between',
|
|
40
|
+
nbetween: '_nbetween',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
44
|
+
'authorization',
|
|
45
|
+
'cookie',
|
|
46
|
+
'cookie2',
|
|
47
|
+
'proxy-authorization',
|
|
48
|
+
'x-api-key',
|
|
49
|
+
'api-key',
|
|
50
|
+
'x-auth-token',
|
|
51
|
+
'x-access-token',
|
|
52
|
+
'x-csrf-token',
|
|
53
|
+
'x-xsrf-token',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
type RequestOptions = Omit<RequestInit, 'headers'> & {
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
interface DirectusListResponse<TData> {
|
|
61
|
+
data?: TData[];
|
|
62
|
+
meta?: {
|
|
63
|
+
total_count?: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface DirectusItemResponse<TData> {
|
|
68
|
+
data?: TData;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergeHeaders(...sources: Array<Record<string, string> | undefined>): Record<string, string> {
|
|
72
|
+
const merged = new Map<string, readonly [name: string, value: string]>();
|
|
73
|
+
|
|
74
|
+
for (const source of sources) {
|
|
75
|
+
if (!source) continue;
|
|
76
|
+
for (const [name, value] of Object.entries(source)) {
|
|
77
|
+
merged.set(name.toLowerCase(), [name, value]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Object.fromEntries(merged.values());
|
|
82
|
+
}
|
|
17
83
|
|
|
84
|
+
function withoutSensitiveHeaders(headers: Record<string, string>): Record<string, string> {
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
Object.entries(headers).filter(([name]) => !SENSITIVE_HEADER_NAMES.has(name.toLowerCase())),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function directusValue(filter: FieldFilter): unknown {
|
|
91
|
+
if (filter.operator === 'null' || filter.operator === 'nnull') return true;
|
|
92
|
+
return filter.value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildDirectusRule(filter: Filter): Record<string, unknown> {
|
|
96
|
+
if ('field' in filter) {
|
|
97
|
+
if (!Object.prototype.hasOwnProperty.call(DIRECTUS_OPERATOR_MAP, filter.operator)) {
|
|
98
|
+
throw new Error(`Unsupported Directus filter operator: ${String(filter.operator)}`);
|
|
99
|
+
}
|
|
100
|
+
const operator = DIRECTUS_OPERATOR_MAP[filter.operator];
|
|
101
|
+
return { [filter.field]: { [operator]: directusValue(filter) } };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (filter.operator !== 'and' && filter.operator !== 'or') {
|
|
105
|
+
throw new Error(`Unsupported Directus logical operator: ${String(filter.operator)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const operator = filter.operator === 'and' ? '_and' : '_or';
|
|
109
|
+
return { [operator]: filter.value.map(buildDirectusRule) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildDirectusFilter(filters?: Filter[]): Record<string, unknown> {
|
|
113
|
+
if (!filters?.length) return {};
|
|
114
|
+
const rules = filters.map(buildDirectusRule);
|
|
18
115
|
return rules.length === 1 ? rules[0] : { _and: rules };
|
|
19
116
|
}
|
|
20
117
|
|
|
118
|
+
function serializeQueryValue(value: unknown): string {
|
|
119
|
+
if (value === null) return 'null';
|
|
120
|
+
if (typeof value === 'string') return value;
|
|
121
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
122
|
+
return String(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const serialized = JSON.stringify(value);
|
|
126
|
+
return serialized === undefined ? String(value) : serialized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function appendQuery(params: URLSearchParams, query?: Record<string, unknown>): void {
|
|
130
|
+
if (!query) return;
|
|
131
|
+
|
|
132
|
+
for (const [name, value] of Object.entries(query)) {
|
|
133
|
+
if (value === undefined) continue;
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
for (const item of value) params.append(name, serializeQueryValue(item));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
params.set(name, serializeQueryValue(value));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function appendSorters(params: URLSearchParams, sorters?: Sort[]): void {
|
|
143
|
+
if (!sorters?.length) return;
|
|
144
|
+
params.set(
|
|
145
|
+
'sort',
|
|
146
|
+
sorters.map(sorter => `${sorter.order === 'desc' ? '-' : ''}${sorter.field}`).join(','),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function appendFilters(params: URLSearchParams, filters?: Filter[]): void {
|
|
151
|
+
const filter = buildDirectusFilter(filters);
|
|
152
|
+
if (Object.keys(filter).length) params.set('filter', JSON.stringify(filter));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fieldsFromMeta(meta?: Record<string, unknown>): string[] | undefined {
|
|
156
|
+
const fields = meta?.fields;
|
|
157
|
+
if (!Array.isArray(fields) || !fields.every(field => typeof field === 'string')) return undefined;
|
|
158
|
+
return fields;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isSameOrigin(apiUrl: string, targetUrl: string): boolean {
|
|
162
|
+
try {
|
|
163
|
+
const api = new URL(apiUrl);
|
|
164
|
+
const target = new URL(targetUrl, apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`);
|
|
165
|
+
return api.origin === target.origin;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function parseResponse<TData>(response: Response): Promise<TData> {
|
|
172
|
+
if (response.status === 204 || response.status === 205) {
|
|
173
|
+
return undefined as unknown as TData;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const contentLength = response.headers?.get('content-length');
|
|
177
|
+
if (contentLength?.trim() === '0') {
|
|
178
|
+
return undefined as unknown as TData;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const body = await response.text();
|
|
182
|
+
if (!body || body.trim() === '') {
|
|
183
|
+
return undefined as unknown as TData;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return JSON.parse(body) as TData;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function fetchData<TData>(
|
|
190
|
+
url: string,
|
|
191
|
+
headers: Record<string, string>,
|
|
192
|
+
init: RequestOptions | undefined,
|
|
193
|
+
errorMessage: (status: number) => string,
|
|
194
|
+
): Promise<TData> {
|
|
195
|
+
const response = await fetch(url, {
|
|
196
|
+
...init,
|
|
197
|
+
headers: mergeHeaders(headers, init?.headers),
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) throw new Error(errorMessage(response.status));
|
|
200
|
+
return parseResponse<TData>(response);
|
|
201
|
+
}
|
|
202
|
+
|
|
21
203
|
export function createDirectusDataProvider(apiUrl: string, token?: string): DataProvider {
|
|
22
|
-
const
|
|
23
|
-
if (token)
|
|
204
|
+
const providerHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
205
|
+
if (token) providerHeaders.Authorization = `Bearer ${token}`;
|
|
206
|
+
const baseUrl = apiUrl.replace(/\/+$/, '');
|
|
24
207
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
208
|
+
function request<TData>(path: string, init?: RequestOptions): Promise<TData> {
|
|
209
|
+
return fetchData<TData>(
|
|
210
|
+
`${baseUrl}${path}`,
|
|
211
|
+
providerHeaders,
|
|
212
|
+
init,
|
|
213
|
+
status => `Directus error: ${status}`,
|
|
214
|
+
);
|
|
29
215
|
}
|
|
30
216
|
|
|
31
217
|
return {
|
|
32
218
|
getApiUrl: () => apiUrl,
|
|
33
219
|
|
|
34
|
-
async getList<
|
|
220
|
+
async getList<TData extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters, meta }: GetListParams): Promise<GetListResult<TData>> {
|
|
35
221
|
const { current = 1, pageSize = 10 } = pagination ?? {};
|
|
36
222
|
const params = new URLSearchParams();
|
|
37
223
|
params.set('limit', String(pageSize));
|
|
38
224
|
params.set('offset', String((current - 1) * pageSize));
|
|
39
225
|
params.set('meta', 'total_count');
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
226
|
+
const fields = fieldsFromMeta(meta);
|
|
227
|
+
if (fields) params.set('fields', fields.join(','));
|
|
228
|
+
appendSorters(params, sorters);
|
|
229
|
+
appendFilters(params, filters);
|
|
44
230
|
|
|
45
|
-
const
|
|
46
|
-
return {
|
|
231
|
+
const response = await request<DirectusListResponse<TData>>(`/items/${resource}?${params}`);
|
|
232
|
+
return {
|
|
233
|
+
data: response?.data ?? [],
|
|
234
|
+
total: response?.meta?.total_count ?? 0,
|
|
235
|
+
};
|
|
47
236
|
},
|
|
48
237
|
|
|
49
|
-
async getOne<
|
|
50
|
-
const params =
|
|
51
|
-
const
|
|
52
|
-
|
|
238
|
+
async getOne<TData extends BaseRecord = BaseRecord>({ resource, id, meta }: GetOneParams): Promise<GetOneResult<TData>> {
|
|
239
|
+
const params = new URLSearchParams();
|
|
240
|
+
const fields = fieldsFromMeta(meta);
|
|
241
|
+
if (fields) params.set('fields', fields.join(','));
|
|
242
|
+
const query = params.size ? `?${params}` : '';
|
|
243
|
+
const response = await request<DirectusItemResponse<TData>>(
|
|
244
|
+
`/items/${resource}/${encodeURIComponent(String(id))}${query}`,
|
|
245
|
+
);
|
|
246
|
+
return { data: response?.data as TData };
|
|
53
247
|
},
|
|
54
248
|
|
|
55
|
-
async create<
|
|
56
|
-
const
|
|
57
|
-
|
|
249
|
+
async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables }: CreateParams<TVariables>): Promise<CreateResult<TData>> {
|
|
250
|
+
const response = await request<DirectusItemResponse<TData>>(`/items/${resource}`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
body: JSON.stringify(variables),
|
|
253
|
+
});
|
|
254
|
+
return { data: response?.data as TData };
|
|
58
255
|
},
|
|
59
256
|
|
|
60
|
-
async update<
|
|
61
|
-
const
|
|
62
|
-
|
|
257
|
+
async update<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, variables }: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
|
|
258
|
+
const response = await request<DirectusItemResponse<TData>>(
|
|
259
|
+
`/items/${resource}/${encodeURIComponent(String(id))}`,
|
|
260
|
+
{ method: 'PATCH', body: JSON.stringify(variables) },
|
|
261
|
+
);
|
|
262
|
+
return { data: response?.data as TData };
|
|
63
263
|
},
|
|
64
264
|
|
|
65
|
-
async deleteOne<
|
|
66
|
-
await request(`/items/${resource}/${id}`, { method: 'DELETE' });
|
|
67
|
-
return { data: { id } as unknown as
|
|
265
|
+
async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id }: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
|
|
266
|
+
await request(`/items/${resource}/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
|
|
267
|
+
return { data: { id } as unknown as TData };
|
|
68
268
|
},
|
|
69
269
|
|
|
70
|
-
async getMany<
|
|
270
|
+
async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids, meta }: GetManyParams): Promise<GetManyResult<TData>> {
|
|
71
271
|
const params = new URLSearchParams();
|
|
72
272
|
params.set('filter', JSON.stringify({ id: { _in: ids } }));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
273
|
+
const fields = fieldsFromMeta(meta);
|
|
274
|
+
if (fields) params.set('fields', fields.join(','));
|
|
275
|
+
const response = await request<DirectusListResponse<TData>>(`/items/${resource}?${params}`);
|
|
276
|
+
return { data: response?.data ?? [] };
|
|
76
277
|
},
|
|
77
278
|
|
|
78
|
-
async custom<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
279
|
+
async custom<TData = unknown, TVariables = unknown>({
|
|
280
|
+
url,
|
|
281
|
+
method,
|
|
282
|
+
payload,
|
|
283
|
+
query,
|
|
284
|
+
headers,
|
|
285
|
+
sorters,
|
|
286
|
+
filters,
|
|
287
|
+
}: CustomParams<TVariables>): Promise<CustomResult<TData>> {
|
|
288
|
+
const parsed = new URL(url, apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`);
|
|
289
|
+
appendQuery(parsed.searchParams, query);
|
|
290
|
+
appendSorters(parsed.searchParams, sorters);
|
|
291
|
+
appendFilters(parsed.searchParams, filters);
|
|
292
|
+
const requestUrl = parsed.toString();
|
|
293
|
+
const sameOrigin = isSameOrigin(apiUrl, requestUrl);
|
|
294
|
+
const requestHeaders = mergeHeaders(
|
|
295
|
+
sameOrigin ? providerHeaders : withoutSensitiveHeaders(providerHeaders),
|
|
296
|
+
headers,
|
|
297
|
+
);
|
|
298
|
+
const data = await fetchData<TData>(
|
|
299
|
+
requestUrl,
|
|
300
|
+
requestHeaders,
|
|
301
|
+
{
|
|
302
|
+
method: method.toUpperCase(),
|
|
303
|
+
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
304
|
+
},
|
|
305
|
+
status => `Custom request failed: ${status}`,
|
|
306
|
+
);
|
|
307
|
+
return { data };
|
|
82
308
|
},
|
|
83
309
|
};
|
|
84
310
|
}
|