@tanglemedia/svelte-starter-directus-api 6.0.2 → 9.0.0-next.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.
@@ -10,9 +10,7 @@ import {
10
10
  updateItems,
11
11
  uploadFiles,
12
12
  type AllCollections,
13
- type DirectusClient,
14
- type RegularCollections,
15
- type RestClient
13
+ type RegularCollections
16
14
  } from '@directus/sdk';
17
15
  import {
18
16
  ApiAdapterAbstract,
@@ -24,25 +22,44 @@ import {
24
22
  type ApiDeleteManyQuery,
25
23
  type ApiFindQuery,
26
24
  type ApiId,
25
+ type ApiPatchQuery,
26
+ type ApiPostQuery,
27
+ type ApiPutQuery,
27
28
  type ApiResponse,
28
29
  type ApiUpdateManyQuery,
29
30
  type BaseApiMethods,
30
31
  type HandleEvent
31
32
  } from '@tanglemedia/svelte-starter-core';
32
- import type { DirectusConfig, SchemaShape } from '../types/adapter.types';
33
+ import type { DirectusAdapterClient, DirectusConfig, SchemaShape } from '../types/adapter.types';
34
+
35
+ type PathKey<Schema extends SchemaShape> = Extract<RegularCollections<Schema>, string>;
36
+
37
+ export type AdapterClient<Schema extends SchemaShape = SchemaShape> = DirectusAdapterClient<Schema>;
38
+
39
+ const allowedQueryKeys = [
40
+ 'fields',
41
+ 'sort',
42
+ 'filter',
43
+ 'limit',
44
+ 'offset',
45
+ 'page',
46
+ 'search',
47
+ 'version',
48
+ 'versionRaw',
49
+ 'export',
50
+ 'group',
51
+ 'aggregate',
52
+ 'deep',
53
+ 'alias'
54
+ ] as const;
55
+
56
+ type Q = Record<string, unknown>;
33
57
 
34
- export type AdapterClient<Schema extends SchemaShape = SchemaShape> = DirectusClient<Schema> &
35
- RestClient<Schema>;
36
-
37
- /**
38
- * TODO: Various types fixes. The current adapter doesn't correctly follow the types defined under the ApiAdapterAbstract.
39
- * See the fetch adapter under the core
40
- */
41
58
  export class DirectusApiAdapter<
42
59
  Schema extends SchemaShape = SchemaShape,
43
- Path extends RegularCollections<Schema> = RegularCollections<Schema>
60
+ Path extends PathKey<Schema> = PathKey<Schema>
44
61
  >
45
- extends ApiAdapterAbstract<DirectusConfig, ApiAdapterRequestConfig, Path, AdapterClient>
62
+ extends ApiAdapterAbstract<DirectusConfig, ApiAdapterRequestConfig, Path, AdapterClient<Schema>>
46
63
  implements ApiAdapterHandle
47
64
  {
48
65
  constructor(
@@ -53,8 +70,7 @@ export class DirectusApiAdapter<
53
70
  }
54
71
 
55
72
  async handle(event: HandleEvent) {
56
- const f = event.fetch;
57
- this.directus.globals.fetch = f;
73
+ this.directus.globals.fetch = event.fetch;
58
74
  }
59
75
 
60
76
  getAdapterClient(): AdapterClient<Schema> | null {
@@ -65,51 +81,43 @@ export class DirectusApiAdapter<
65
81
  const payload = response.data,
66
82
  meta = response.meta as Record<string, unknown>;
67
83
 
68
- if (!meta) {
69
- return {};
70
- }
71
-
72
- if (typeof meta.total === 'undefined') {
84
+ if (!meta || typeof meta.total === 'undefined') {
73
85
  return {};
74
86
  }
75
87
 
76
88
  const displayed = Array.isArray(payload) ? payload.length : 0;
77
-
78
89
  const total = Number(meta.total);
79
90
 
80
- // todo: Transform or attempt to get meta information
81
- // consider pulling count based on existing filters in order to determine pagination
82
91
  return { total, displayed };
83
92
  }
84
93
 
85
- async includeTotals(collection: AllCollections<Schema>, query?: ApiFindQuery<any>) {
94
+ async includeTotals(collection: Path, query?: ApiFindQuery<any>) {
86
95
  if (true !== this.config.configuration.includeTotals) {
87
96
  return null;
88
97
  }
89
98
 
90
99
  const { filter, search } = query || {};
91
-
92
100
  // default to count. Note that there is a bug in directus where
93
101
  // the wrong counts are being returned if there are nested filters.
94
102
  // A quick fix is to set totalAggregate to countDistinct
95
103
  const k = this.config.configuration.totalAggregate || 'count';
96
104
  const f = this.config.configuration.totalField || 'id';
97
105
 
98
- const data = await this.directus.request(
99
- aggregate(collection, {
106
+ const data = await this.directus.request<Array<Record<string, unknown>>>(
107
+ aggregate(collection as unknown as AllCollections<Schema>, {
100
108
  aggregate: { [k]: f },
101
109
  query: {
102
110
  ...(filter ? { filter } : {}),
103
111
  ...(search ? { search } : {})
104
112
  }
105
- })
113
+ } as never)
106
114
  );
107
115
 
108
116
  let total = 0;
109
117
 
110
118
  if (data && data.length === 1) {
111
119
  const c = data[0][k];
112
- total = c && typeof c === 'object' ? Number(c[f]) : Number(c);
120
+ total = c && typeof c === 'object' ? Number((c as Record<string, unknown>)[f]) : Number(c);
113
121
  }
114
122
 
115
123
  return isNaN(total) ? 0 : total;
@@ -118,157 +126,269 @@ export class DirectusApiAdapter<
118
126
  async transformResponse<T, M extends object = AnyObject>(
119
127
  res: { data: T; meta?: AnyObject },
120
128
  status: number = 200
121
- ): Promise<ApiResponse<T, Response, M>> {
129
+ ): Promise<ApiResponse<T, null, M>> {
122
130
  return {
123
- body: await this.envelopeResponse<T, M>(
131
+ body: (await this.envelopeResponse<T, M>(
124
132
  res.data,
125
133
  (res.meta ? this.normalizeMeta(res as AnyObject) : {}) as M
126
- ),
127
- status: status,
134
+ )) as ApiResponse<T, null, M>['body'],
135
+ status,
128
136
  statusText: 'OK',
129
137
  headers: {},
130
138
  adapterResponse: null
131
139
  };
132
140
  }
133
141
 
134
- public getConfig(configuration?: unknown): DirectusConfig {
135
- const client = this.directus;
142
+ normalizeQuery(config: ApiAdapterRequestConfig = {}, merge?: Q) {
143
+ const q = { ...((config?.params as Q | undefined) || {}) };
144
+ const c = { ...config } as Q;
145
+ const r = allowedQueryKeys.reduce<Q>(
146
+ (acc, k) => (typeof c[k] !== 'undefined' ? ((acc[k] = c[k]), acc) : acc),
147
+ {}
148
+ );
136
149
 
137
- // Add the 'configuration' property to the returned object
138
- const config: DirectusConfig = {
139
- ...this.config,
140
- configuration: configuration ?? this.config.configuration ?? {} // Use the provided configuration or an empty object if not provided
150
+ return {
151
+ ...q,
152
+ ...r,
153
+ ...(merge || {})
141
154
  };
155
+ }
156
+
157
+ getId(data: object, config?: ApiAdapterRequestConfig): ApiId | null {
158
+ const d = data as Record<string, unknown>;
159
+ const q = config?.params as Q | undefined;
160
+
161
+ if (typeof d.id === 'string' || typeof d.id === 'number') {
162
+ return d.id;
163
+ }
164
+
165
+ if (q && (typeof q.id === 'string' || typeof q.id === 'number')) {
166
+ return q.id;
167
+ }
142
168
 
143
- return Object.assign(client, config);
169
+ return null;
144
170
  }
145
171
 
146
- public async request<T>(
172
+ stripId(config?: ApiAdapterRequestConfig) {
173
+ const q = config?.params as Q | undefined;
174
+
175
+ if (!q || typeof q.id === 'undefined') {
176
+ return config;
177
+ }
178
+
179
+ const { id: _id, ...params } = q;
180
+
181
+ return {
182
+ ...config,
183
+ params
184
+ };
185
+ }
186
+
187
+ public async request<T, R = T>(
147
188
  method: BaseApiMethods,
148
- url: string,
149
- query?: Record<string, unknown>
150
- ): Promise<T> {
189
+ url: Path,
190
+ query?: ApiAdapterRequestConfig
191
+ ): Promise<ApiResponse<R>> {
151
192
  try {
152
- const response = await this.directus.request<T>(() => {
153
- const params = JSON.stringify(query);
154
- return {
155
- path: `${url}?${params}`,
156
- method: method
157
- };
158
- });
159
- return response;
193
+ const { body, headers } = query || {};
194
+ const params = this.normalizeQuery(query);
195
+
196
+ const data = await this.directus.request<R>(
197
+ (() =>
198
+ ({
199
+ path: url,
200
+ method,
201
+ headers,
202
+ params,
203
+ ...(typeof body === 'undefined'
204
+ ? {}
205
+ : {
206
+ body:
207
+ body instanceof FormData || typeof body === 'string'
208
+ ? body
209
+ : JSON.stringify(body)
210
+ })
211
+ })) as never
212
+ );
213
+
214
+ return this.transformResponse({ data }, method === 'POST' ? 201 : method === 'DELETE' ? 202 : 200);
160
215
  } catch (error) {
161
216
  console.error(`Error request:`, error);
162
217
  throw error;
163
218
  }
164
219
  }
165
220
 
166
- public async find<T, Q = T, R = T>(
221
+ public async find<T, F = T, R = T>(
167
222
  collection: Path,
168
- query: ApiFindQuery<Q>
223
+ query: ApiFindQuery<F>,
224
+ config?: ApiAdapterRequestConfig
169
225
  ): Promise<ApiResponse<R[]>> {
170
- const response = (await this.directus.request<T>(readItems(collection, query))) as R[];
226
+ const q = this.normalizeQuery(config, query as Q);
227
+ const data = await this.directus.request<R[]>(
228
+ readItems(collection as unknown as RegularCollections<Schema>, q as never)
229
+ );
171
230
  const total = await this.includeTotals(collection, query);
231
+
172
232
  if (null === total) {
173
- return this.transformResponse({ data: response });
233
+ return this.transformResponse({ data });
174
234
  }
175
- // console.log('find meta', response, total)
176
235
 
177
- return this.transformResponse({ data: response, meta: { total } });
236
+ return this.transformResponse({ data, meta: { total } });
178
237
  }
179
238
 
180
- public async findOne<T>(
239
+ public async findOne<T, R = T>(
181
240
  collection: Path,
182
- key?: ApiId,
183
- query?: Record<string, unknown>
184
- ): Promise<ApiResponse<T>> {
185
- const data = await this.directus.request<T>(readItem(collection, key, query));
241
+ key: ApiId,
242
+ query?: ApiAdapterRequestConfig
243
+ ): Promise<ApiResponse<R>> {
244
+ const q = this.normalizeQuery(query);
245
+ const data = await this.directus.request<R>(
246
+ readItem(collection as unknown as RegularCollections<Schema>, key, q as never)
247
+ );
186
248
  return this.transformResponse({ data });
187
249
  }
188
250
 
189
- public async aggregate<T, Q = T, R = T>(
251
+ public async aggregate<T, F = T, R = T>(
190
252
  collection: Path,
191
- query: ApiAggregateQuery<Q>
253
+ query: ApiAggregateQuery<F>,
254
+ config?: ApiAdapterRequestConfig
192
255
  ): Promise<ApiResponse<R>> {
193
- const { aggregate: aggregate_, ...rest } = query || {};
194
-
195
- const data = await this.directus.request<T>(
196
- aggregate(collection, {
197
- aggregate: aggregate_
198
- ? aggregate_
199
- : {
200
- count: 'id'
201
- },
256
+ const { aggregate: a, ...rest } = this.normalizeQuery(config, query as Q);
257
+
258
+ const data = await this.directus.request<R>(
259
+ aggregate(collection as unknown as AllCollections<Schema>, {
260
+ aggregate: a ? a : { count: 'id' },
202
261
  query: rest
203
- })
262
+ } as never)
204
263
  );
205
264
 
206
265
  return this.transformResponse({ data });
207
266
  }
208
267
 
209
- public async patch<T>(
268
+ public async patch<T extends object, R = T>(
210
269
  collection: Path,
211
270
  key: ApiId,
212
- query?: Record<string, unknown>
213
- ): Promise<ApiResponse<T>> {
214
- const data = await this.directus.request<T>(updateItem(collection, key, query));
215
- return this.transformResponse({ data });
271
+ data: ApiPatchQuery<T>,
272
+ config?: ApiAdapterRequestConfig
273
+ ): Promise<ApiResponse<R>> {
274
+ const q = this.normalizeQuery(config);
275
+ const res = await this.directus.request<R>(
276
+ updateItem(collection as never, key, data as never, q as never)
277
+ );
278
+ return this.transformResponse({ data: res });
216
279
  }
217
280
 
218
- public async post<T>(
281
+ public async post<T extends object, R = T>(
219
282
  collection: Path,
220
- data: AnyObject,
221
- query?: Record<string, unknown>
222
- ): Promise<ApiResponse<T>> {
223
- const response = await this.directus.request<T>(createItem(collection, data, query));
224
- return this.transformResponse({ data: response }, 201);
283
+ data: ApiPostQuery<T>,
284
+ config?: ApiAdapterRequestConfig
285
+ ): Promise<ApiResponse<R>> {
286
+ const q = this.normalizeQuery(config);
287
+ const res = await this.directus.request<R>(
288
+ createItem(collection as never, data as never, q as never)
289
+ );
290
+ return this.transformResponse({ data: res }, 201);
225
291
  }
226
292
 
227
- public async delete<T>(collection: Path, key?: ApiId): Promise<ApiResponse<T>> {
228
- const data = await this.directus.request<T>(deleteItem(collection, key));
293
+ public async delete<T, R = T>(
294
+ collection: Path,
295
+ key: ApiId,
296
+ config?: ApiAdapterRequestConfig
297
+ ): Promise<ApiResponse<R>> {
298
+ void config;
299
+ const data = await this.directus.request<R>(deleteItem(collection as never, key));
229
300
  return this.transformResponse({ data }, 202);
230
301
  }
231
302
 
232
303
  public async put<T extends object, R = T>(
233
304
  collection: Path,
234
- key?: ApiId,
235
- payload?: AnyObject,
236
- query?: Record<string, unknown>
305
+ data: ApiPutQuery<T>,
306
+ config?: ApiAdapterRequestConfig
307
+ ): Promise<ApiResponse<R>>;
308
+ public async put<T extends object, R = T>(
309
+ collection: Path,
310
+ key: ApiId,
311
+ data?: AnyObject,
312
+ config?: ApiAdapterRequestConfig
313
+ ): Promise<ApiResponse<R>>;
314
+ public async put<T extends object, R = T>(
315
+ collection: Path,
316
+ keyOrData: ApiId | ApiPutQuery<T>,
317
+ dataOrConfig?: AnyObject | ApiAdapterRequestConfig,
318
+ config?: ApiAdapterRequestConfig
237
319
  ): Promise<ApiResponse<R>> {
238
- if (key) {
239
- const data = (await this.directus.request<T>(
240
- updateItem(collection, key, payload, query)
241
- )) as R;
242
- return this.transformResponse({ data }, 201);
243
- } else {
244
- console.error(`Error updating all ${collection}: no key specified`);
320
+ const key =
321
+ typeof keyOrData === 'string' || typeof keyOrData === 'number'
322
+ ? keyOrData
323
+ : this.getId(keyOrData, dataOrConfig as ApiAdapterRequestConfig | undefined);
324
+ const data =
325
+ typeof keyOrData === 'string' || typeof keyOrData === 'number'
326
+ ? (dataOrConfig as AnyObject | undefined)
327
+ : keyOrData;
328
+ const q =
329
+ typeof keyOrData === 'string' || typeof keyOrData === 'number'
330
+ ? config
331
+ : (dataOrConfig as ApiAdapterRequestConfig | undefined);
332
+
333
+ if (!key) {
245
334
  throw new Error('No key specified');
246
335
  }
336
+
337
+ const res = await this.directus.request<R>(
338
+ updateItem(collection as never, key, data as never, this.normalizeQuery(this.stripId(q)) as never)
339
+ );
340
+
341
+ return this.transformResponse({ data: res }, 201);
247
342
  }
248
343
 
249
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
250
- public async upload<T>(path: string = '', data: FormData): Promise<ApiResponse<T>> {
251
- const response = await this.directus.request<T>(uploadFiles(data));
252
- return this.transformResponse({ data: response }, 201);
344
+ public async upload<T, R = T>(
345
+ path: Path,
346
+ data: FormData,
347
+ config?: ApiAdapterRequestConfig & { method?: BaseApiMethods }
348
+ ): Promise<ApiResponse<R>> {
349
+ void path;
350
+ void config;
351
+ const res = await this.directus.request<R>(uploadFiles(data));
352
+ return this.transformResponse({ data: res }, 201);
253
353
  }
254
354
 
255
- async createMany<T, R = T>(path: Path, body: ApiCreateManyQuery<T>) {
256
- const data = await this.directus.request<T>(createItems(path, body.data, body?.query));
257
- return this.transformResponse<R[]>({ data }, 200);
355
+ async createMany<T, R = T>(
356
+ path: Path,
357
+ body: ApiCreateManyQuery<T>,
358
+ config?: ApiAdapterRequestConfig
359
+ ): Promise<ApiResponse<R[]>> {
360
+ const q = this.normalizeQuery(config, ((body as Q).query as Q | undefined) || {});
361
+ const data = await this.directus.request<R[]>(
362
+ createItems(path as never, body.data as never, q as never)
363
+ );
364
+ return this.transformResponse({ data }, 200);
258
365
  }
259
366
 
260
- async updateMany<T, R = T>(path: Path, body: ApiUpdateManyQuery<T>) {
367
+ async updateMany<T, R = T>(
368
+ path: Path,
369
+ body: ApiUpdateManyQuery<T>,
370
+ config?: ApiAdapterRequestConfig
371
+ ): Promise<ApiResponse<R[]>> {
261
372
  if (!body.ids) {
262
373
  throw Error('You must provide an array of keys to update');
263
374
  }
264
- const data = await this.directus.request<T>(
265
- updateItems(path, body.ids || [], body.data, body?.query)
375
+
376
+ const q = this.normalizeQuery(config, ((body as Q).query as Q | undefined) || {});
377
+ const ids = body.ids as string[] | number[];
378
+ const data = await this.directus.request<R[]>(
379
+ updateItems(path as never, ids, body.data as never, q as never)
266
380
  );
267
- return this.transformResponse<R[]>({ data }, 200);
381
+ return this.transformResponse({ data }, 200);
268
382
  }
269
383
 
270
- async deleteMany<R = null>(path: Path, body: ApiDeleteManyQuery<ApiId>) {
271
- const data = await this.directus.request(deleteItems(path, body.ids));
272
- return this.transformResponse<R>({ data } as R, 201);
384
+ async deleteMany<R>(
385
+ path: Path,
386
+ body: ApiDeleteManyQuery<ApiId>,
387
+ config?: ApiAdapterRequestConfig
388
+ ): Promise<ApiResponse<R[]>> {
389
+ void config;
390
+ const ids = body.ids as string[] | number[];
391
+ const data = await this.directus.request<R[]>(deleteItems(path as never, ids));
392
+ return this.transformResponse({ data }, 201);
273
393
  }
274
394
  }
@@ -1,17 +1,25 @@
1
1
  import type {
2
2
  AdapterProviderRegister,
3
+ ApiAdapterRequestConfig,
3
4
  ApiAdapterInterface,
4
5
  ApiAdapterProviderInterface
5
6
  } from '@tanglemedia/svelte-starter-core';
6
7
  import { DirectusApiAdapter } from '../adapter/api-adapter';
7
- import type { DirectusConfig, SchemaShape } from '../types/adapter.types';
8
+ import type { DirectusAdapterClient, DirectusConfig, SchemaShape } from '../types/adapter.types';
8
9
  import { createDirectusClientFactory } from './directus.factory';
9
10
 
10
11
  export class DirectusApiProvider implements ApiAdapterProviderInterface<DirectusConfig> {
11
12
  async loadAdapter(
12
13
  key?: string,
13
14
  config?: DirectusConfig
14
- ): Promise<ApiAdapterInterface<DirectusConfig>> {
15
+ ): Promise<
16
+ ApiAdapterInterface<
17
+ DirectusConfig,
18
+ ApiAdapterRequestConfig,
19
+ string,
20
+ DirectusAdapterClient<SchemaShape>
21
+ >
22
+ > {
15
23
  if (!config) {
16
24
  throw new Error('Missing adapter configuration');
17
25
  }
@@ -21,7 +29,7 @@ export class DirectusApiProvider implements ApiAdapterProviderInterface<Directus
21
29
  return new DirectusApiAdapter(config, client);
22
30
  }
23
31
 
24
- createDirectusClient<T extends object>(config: DirectusConfig) {
32
+ createDirectusClient<T extends SchemaShape>(config: DirectusConfig): DirectusAdapterClient<T> {
25
33
  // todo: Determine if we can overwrite the fetch globals during runtime
26
34
  return createDirectusClientFactory<T>(config);
27
35
  }