@svadmin/directus 0.9.6 → 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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/data-provider.ts +273 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/directus",
3
- "version": "0.9.6",
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.25.3"
23
+ "@svadmin/core": "^0.41.1"
24
24
  },
25
25
  "license": "MIT",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/zuohuadong/svadmin.git",
28
+ "url": "https://github.com/vibeunion/svadmin.git",
29
29
  "directory": "packages/directus"
30
30
  },
31
31
  "peerDependenciesMeta": {
@@ -1,83 +1,310 @@
1
1
  import type {
2
- DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
3
- CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
4
- GetManyParams, GetManyResult, CustomParams, CustomResult, Sort, Filter, LogicalFilter,
5
- BaseRecord
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,
6
22
  } from '@svadmin/core';
7
23
 
8
- function buildDirectusFilter(filters?: (Filter | LogicalFilter | any)[]): Record<string, unknown> {
9
- if (!filters?.length) return {};
10
- const opMap: Record<string, string> = { eq: '_eq', ne: '_neq', lt: '_lt', gt: '_gt', lte: '_lte', gte: '_gte', contains: '_contains', in: '_in', null: '_null', and: '_and', or: '_or' };
11
-
12
- const rules = filters.map(f => {
13
- if ('field' in f) return { [f.field as string]: { [opMap[f.operator as string] ?? '_eq']: f.value } };
14
- return { [opMap[f.operator]]: (f.value as any[]).map(sub => buildDirectusFilter([sub])) };
15
- });
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
+ }
16
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);
17
115
  return rules.length === 1 ? rules[0] : { _and: rules };
18
116
  }
19
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
+
20
203
  export function createDirectusDataProvider(apiUrl: string, token?: string): DataProvider {
21
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
22
- if (token) headers['Authorization'] = `Bearer ${token}`;
204
+ const providerHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
205
+ if (token) providerHeaders.Authorization = `Bearer ${token}`;
206
+ const baseUrl = apiUrl.replace(/\/+$/, '');
23
207
 
24
- async function request<T>(path: string, opts?: RequestInit): Promise<T> {
25
- const res = await fetch(`${apiUrl}${path}`, { ...opts, headers: { ...headers, ...opts?.headers } });
26
- if (!res.ok) throw new Error(`Directus error: ${res.status}`);
27
- return res.json();
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
+ );
28
215
  }
29
216
 
30
217
  return {
31
218
  getApiUrl: () => apiUrl,
32
219
 
33
- async getList<T extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters, meta }: GetListParams): Promise<GetListResult<T>> {
220
+ async getList<TData extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters, meta }: GetListParams): Promise<GetListResult<TData>> {
34
221
  const { current = 1, pageSize = 10 } = pagination ?? {};
35
222
  const params = new URLSearchParams();
36
223
  params.set('limit', String(pageSize));
37
224
  params.set('offset', String((current - 1) * pageSize));
38
225
  params.set('meta', 'total_count');
39
- if (meta?.fields) params.set('fields', (meta.fields as string[]).join(','));
40
- if (sorters?.length) params.set('sort', sorters.map((s: Sort) => `${s.order === 'desc' ? '-' : ''}${s.field}`).join(','));
41
- const filter = buildDirectusFilter(filters);
42
- if (Object.keys(filter).length) params.set('filter', JSON.stringify(filter));
226
+ const fields = fieldsFromMeta(meta);
227
+ if (fields) params.set('fields', fields.join(','));
228
+ appendSorters(params, sorters);
229
+ appendFilters(params, filters);
43
230
 
44
- const data = await request<any>(`/items/${resource}?${params}`);
45
- return { data: data.data ?? [], total: data.meta?.total_count ?? 0 };
231
+ const response = await request<DirectusListResponse<TData>>(`/items/${resource}?${params}`);
232
+ return {
233
+ data: response?.data ?? [],
234
+ total: response?.meta?.total_count ?? 0,
235
+ };
46
236
  },
47
237
 
48
- async getOne<T extends BaseRecord = BaseRecord>({ resource, id, meta }: GetOneParams): Promise<GetOneResult<T>> {
49
- const params = meta?.fields ? `?fields=${(meta.fields as string[]).join(',')}` : '';
50
- const data = await request<any>(`/items/${resource}/${id}${params}`);
51
- return { data: data.data as unknown as T };
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 };
52
247
  },
53
248
 
54
- async create<T extends BaseRecord = BaseRecord>({ resource, variables }: CreateParams): Promise<CreateResult<T>> {
55
- const data = await request<any>(`/items/${resource}`, { method: 'POST', body: JSON.stringify(variables) });
56
- return { data: data.data as unknown as T };
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 };
57
255
  },
58
256
 
59
- async update<T extends BaseRecord = BaseRecord>({ resource, id, variables }: UpdateParams): Promise<UpdateResult<T>> {
60
- const data = await request<any>(`/items/${resource}/${id}`, { method: 'PATCH', body: JSON.stringify(variables) });
61
- return { data: data.data as unknown as T };
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 };
62
263
  },
63
264
 
64
- async deleteOne<T extends BaseRecord = BaseRecord>({ resource, id }: DeleteParams): Promise<DeleteResult<T>> {
65
- await request(`/items/${resource}/${id}`, { method: 'DELETE' });
66
- return { data: { id } as unknown as T };
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 };
67
268
  },
68
269
 
69
- async getMany<T extends BaseRecord = BaseRecord>({ resource, ids, meta }: GetManyParams): Promise<GetManyResult<T>> {
270
+ async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids, meta }: GetManyParams): Promise<GetManyResult<TData>> {
70
271
  const params = new URLSearchParams();
71
272
  params.set('filter', JSON.stringify({ id: { _in: ids } }));
72
- if (meta?.fields) params.set('fields', (meta.fields as string[]).join(','));
73
- const data = await request<any>(`/items/${resource}?${params}`);
74
- return { data: data.data ?? [] };
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 ?? [] };
75
277
  },
76
278
 
77
- async custom<T = unknown>({ url, method, payload, headers: h }: CustomParams): Promise<CustomResult<T>> {
78
- const res = await fetch(url, { method: method.toUpperCase(), headers: { ...headers, ...h }, body: payload ? JSON.stringify(payload) : undefined });
79
- if (!res.ok) throw new Error(`Custom request failed: ${res.status}`);
80
- return { data: (await res.json()) as unknown as T };
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 };
81
308
  },
82
309
  };
83
310
  }