@directus/api 31.0.0 → 32.0.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.
Files changed (135) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +11 -8
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +11 -8
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/server.js +26 -1
  20. package/dist/controllers/settings.js +9 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/database/helpers/fn/types.js +3 -3
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  33. package/dist/database/helpers/schema/types.d.ts +5 -0
  34. package/dist/database/helpers/schema/types.js +6 -0
  35. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  36. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  37. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  38. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  39. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  40. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  41. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  43. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  44. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  45. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  46. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  47. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  48. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  49. package/dist/flows.js +1 -0
  50. package/dist/mcp/schema.d.ts +14 -14
  51. package/dist/mcp/schema.js +6 -6
  52. package/dist/mcp/server.d.ts +9 -3
  53. package/dist/mcp/server.js +1 -1
  54. package/dist/mcp/tools/collections.d.ts +1 -1
  55. package/dist/mcp/tools/fields.d.ts +1 -1
  56. package/dist/mcp/tools/files.d.ts +25 -25
  57. package/dist/mcp/tools/flows.d.ts +36 -36
  58. package/dist/mcp/tools/folders.d.ts +18 -18
  59. package/dist/mcp/tools/items.d.ts +18 -18
  60. package/dist/mcp/tools/operations.d.ts +19 -19
  61. package/dist/mcp/tools/prompts/items.md +1 -1
  62. package/dist/metrics/lib/create-metrics.js +16 -25
  63. package/dist/middleware/collection-exists.js +2 -2
  64. package/dist/operations/mail/index.js +3 -1
  65. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  66. package/dist/operations/mail/rate-limiter.js +29 -0
  67. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  68. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  69. package/dist/schedules/metrics.js +6 -2
  70. package/dist/schedules/project.d.ts +4 -0
  71. package/dist/schedules/project.js +27 -0
  72. package/dist/services/collections.d.ts +3 -3
  73. package/dist/services/collections.js +16 -1
  74. package/dist/services/fields.d.ts +21 -5
  75. package/dist/services/fields.js +105 -28
  76. package/dist/services/graphql/resolvers/query.js +1 -1
  77. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  78. package/dist/services/graphql/schema/parse-query.js +8 -8
  79. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  80. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  81. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  82. package/dist/services/import-export.d.ts +9 -1
  83. package/dist/services/import-export.js +318 -101
  84. package/dist/services/items.d.ts +1 -1
  85. package/dist/services/items.js +36 -20
  86. package/dist/services/mail/index.js +2 -0
  87. package/dist/services/mail/rate-limiter.d.ts +1 -0
  88. package/dist/services/mail/rate-limiter.js +29 -0
  89. package/dist/services/meta.js +28 -24
  90. package/dist/services/schema.js +4 -1
  91. package/dist/services/server.d.ts +1 -0
  92. package/dist/services/server.js +14 -18
  93. package/dist/services/settings.d.ts +2 -1
  94. package/dist/services/settings.js +15 -0
  95. package/dist/services/tus/server.js +14 -9
  96. package/dist/telemetry/lib/get-report.js +4 -4
  97. package/dist/telemetry/lib/send-report.d.ts +6 -1
  98. package/dist/telemetry/lib/send-report.js +3 -1
  99. package/dist/telemetry/types/report.d.ts +17 -1
  100. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  101. package/dist/telemetry/utils/get-settings.js +14 -0
  102. package/dist/test-utils/README.md +760 -0
  103. package/dist/test-utils/cache.d.ts +51 -0
  104. package/dist/test-utils/cache.js +59 -0
  105. package/dist/test-utils/database.d.ts +48 -0
  106. package/dist/test-utils/database.js +52 -0
  107. package/dist/test-utils/emitter.d.ts +35 -0
  108. package/dist/test-utils/emitter.js +38 -0
  109. package/dist/test-utils/fields-service.d.ts +28 -0
  110. package/dist/test-utils/fields-service.js +36 -0
  111. package/dist/test-utils/items-service.d.ts +23 -0
  112. package/dist/test-utils/items-service.js +37 -0
  113. package/dist/test-utils/knex.d.ts +164 -0
  114. package/dist/test-utils/knex.js +268 -0
  115. package/dist/test-utils/schema.d.ts +26 -0
  116. package/dist/test-utils/schema.js +35 -0
  117. package/dist/types/auth.d.ts +0 -2
  118. package/dist/utils/apply-diff.js +15 -0
  119. package/dist/utils/create-admin.d.ts +11 -0
  120. package/dist/utils/create-admin.js +50 -0
  121. package/dist/utils/get-schema.js +5 -3
  122. package/dist/utils/get-snapshot-diff.js +49 -5
  123. package/dist/utils/get-snapshot.js +13 -7
  124. package/dist/utils/sanitize-schema.d.ts +11 -4
  125. package/dist/utils/sanitize-schema.js +9 -6
  126. package/dist/utils/schedule.js +15 -19
  127. package/dist/utils/validate-diff.js +31 -0
  128. package/dist/utils/validate-snapshot.js +7 -0
  129. package/dist/websocket/controllers/hooks.js +12 -20
  130. package/dist/websocket/messages.d.ts +3 -3
  131. package/package.json +63 -65
  132. package/dist/cli/utils/defaults.d.ts +0 -4
  133. package/dist/cli/utils/defaults.js +0 -17
  134. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  135. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -0,0 +1,760 @@
1
+ # Service Test Mocks
2
+
3
+ Shared mocking utilities for service layer tests. These utilities help reduce code duplication and provide consistent
4
+ mocking patterns across service tests.
5
+
6
+ ## Overview
7
+
8
+ This directory contains mock implementations for commonly used modules in service tests:
9
+
10
+ - **[knex.ts](#knexts)** - Knex instance, query tracker, and schema builder mocks
11
+ - **[database.ts](#databasets)** - Database client and transaction mocks
12
+ - **[cache.ts](#cachets)** - Cache system mocks
13
+ - **[schema.ts](#schemats)** - Schema inspector mocks
14
+ - **[emitter.ts](#emitterts)** - Event emitter mocks
15
+ - **[items-service.ts](#items-servicets)** - ItemsService mocks
16
+ - **[fields-service.ts](#fields-servicets)** - FieldsService mocks
17
+ - **[test-helpers.ts](#test-helpersts)** - Test data factory functions
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
23
+
24
+ // Set up mocks
25
+ vi.mock('../../src/database/index', async () => {
26
+ const { mockDatabase } = await import('../__mocks__/database.js');
27
+ return mockDatabase();
28
+ });
29
+
30
+ vi.mock('../cache.js', async () => {
31
+ const { mockCache } = await import('../__mocks__/cache.js');
32
+ return mockCache();
33
+ });
34
+
35
+ describe('YourService Tests', () => {
36
+ const { db, tracker, mockSchemaBuilder } = createMockKnex();
37
+
38
+ afterEach(() => {
39
+ resetKnexMocks(tracker, mockSchemaBuilder);
40
+ });
41
+
42
+ // Your tests here
43
+ });
44
+ ```
45
+
46
+ ## File Documentation
47
+
48
+ ### knex.ts
49
+
50
+ Provides Knex mocking utilities including the database client, query tracker, and schema builder.
51
+
52
+ #### `createMockKnex()`
53
+
54
+ Creates a complete mocked Knex instance with tracker and schema builder support.
55
+
56
+ **Returns:** `{ db, tracker, mockSchemaBuilder }`
57
+
58
+ - `db`: Mocked Knex client (connected to knex-mock-client)
59
+ - `tracker`: Query tracker for mocking database responses
60
+ - `mockSchemaBuilder`: Mocked schema builder with createTable, dropTable, alterTable, etc.
61
+
62
+ **Example:**
63
+
64
+ ```typescript
65
+ const { db, tracker, mockSchemaBuilder } = createMockKnex();
66
+
67
+ // Mock query responses
68
+ tracker.on.select('users').response([{ id: 1, name: 'John' }]);
69
+ tracker.on.insert('users').response([1]);
70
+
71
+ // Use in service constructor
72
+ const service = new YourService({ knex: db, schema });
73
+
74
+ // Verify schema operations
75
+ expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));
76
+ ```
77
+
78
+ #### `createMockTableBuilder()`
79
+
80
+ Creates a mock Knex table builder with all column types, modifiers, and constraints.
81
+
82
+ **Returns:** Mock table builder with chainable methods
83
+
84
+ **Example:**
85
+
86
+ ```typescript
87
+ const table = createMockTableBuilder();
88
+ table.string('name', 255).notNullable().index();
89
+ table.integer('age').unsigned().nullable();
90
+ ```
91
+
92
+ #### `setupSystemCollectionMocks(tracker)`
93
+
94
+ Sets up default CRUD operation mocks for all Directus system collections (directus_collections, directus_fields, etc.).
95
+
96
+ **Parameters:**
97
+
98
+ - `tracker`: The knex-mock-client tracker instance
99
+
100
+ **Example:**
101
+
102
+ ```typescript
103
+ const { tracker } = createMockKnex();
104
+ setupSystemCollectionMocks(tracker);
105
+ // Now all system collection queries return empty arrays by default
106
+ ```
107
+
108
+ #### `resetKnexMocks(tracker, mockSchemaBuilder)`
109
+
110
+ Resets all mock states. Should be called in `afterEach` hooks.
111
+
112
+ **Parameters:**
113
+
114
+ - `tracker`: The knex-mock-client tracker instance
115
+ - `mockSchemaBuilder`: The mock schema builder object from createMockKnex
116
+
117
+ **Example:**
118
+
119
+ ```typescript
120
+ const { db, tracker, mockSchemaBuilder } = createMockKnex();
121
+
122
+ afterEach(() => {
123
+ resetKnexMocks(tracker, mockSchemaBuilder);
124
+ });
125
+ ```
126
+
127
+ #### `mockCreateTable()`
128
+
129
+ Creates a mock for `db.schema.createTable` that executes the callback with a mock table builder.
130
+
131
+ **Returns:** `vi.fn()` that can be assigned to `db.schema.createTable`
132
+
133
+ **Example:**
134
+
135
+ ```typescript
136
+ const { db } = createMockKnex();
137
+ const createTableSpy = mockCreateTable();
138
+ db.schema.createTable = createTableSpy as any;
139
+
140
+ await db.schema.createTable('users', (table) => {
141
+ table.increments('id');
142
+ table.string('name');
143
+ });
144
+
145
+ expect(createTableSpy).toHaveBeenCalledWith('users', expect.any(Function));
146
+ ```
147
+
148
+ #### `mockAlterTable()`
149
+
150
+ Creates a mock for `db.schema.alterTable` that executes the callback with a mock table builder.
151
+
152
+ **Returns:** `vi.fn()` that can be assigned to `db.schema.alterTable`
153
+
154
+ **Example:**
155
+
156
+ ```typescript
157
+ const { db } = createMockKnex();
158
+ const alterTableSpy = mockAlterTable();
159
+ db.schema.alterTable = alterTableSpy as any;
160
+
161
+ await db.schema.alterTable('users', (table) => {
162
+ table.string('email');
163
+ });
164
+ ```
165
+
166
+ #### `mockSchemaTable()`
167
+
168
+ Creates a mock for `db.schema.table` that executes the callback with a mock table builder.
169
+
170
+ **Returns:** `vi.fn()` that can be assigned to `db.schema.table`
171
+
172
+ **Example:**
173
+
174
+ ```typescript
175
+ const { db } = createMockKnex();
176
+ const schemaTableSpy = mockSchemaTable();
177
+ db.schema.table = schemaTableSpy as any;
178
+
179
+ await db.schema.table('users', (table) => {
180
+ table.dropColumn('old_field');
181
+ });
182
+ ```
183
+
184
+ ---
185
+
186
+ ### database.ts
187
+
188
+ Provides database module mocking utilities including database client detection and transaction handling.
189
+
190
+ #### `mockDatabase(client?)`
191
+
192
+ Creates a standard database module mock for service tests.
193
+
194
+ **Parameters:**
195
+
196
+ - `client` (optional): Database client to mock (default: `'postgres'`)
197
+ - Supported values: `'postgres'`, `'mysql'`, `'sqlite3'`, `'mssql'`, `'oracledb'`, `'cockroachdb'`
198
+
199
+ **Returns:** Mock module object with `getDatabaseClient` and `getSchemaInspector`
200
+
201
+ **Example:**
202
+
203
+ ```typescript
204
+ // Standard PostgreSQL mock
205
+ vi.mock('../../src/database/index', async () => {
206
+ const { mockDatabase } = await import('../__mocks__/database.js');
207
+ return mockDatabase();
208
+ });
209
+
210
+ // MySQL-specific mock
211
+ vi.mock('../../src/database/index', async () => {
212
+ const { mockDatabase } = await import('../__mocks__/database.js');
213
+ return mockDatabase('mysql');
214
+ });
215
+
216
+ // Dynamically change client during tests
217
+ import { getDatabaseClient } from '../../src/database/index.js';
218
+ vi.mocked(getDatabaseClient).mockReturnValue('sqlite3');
219
+ ```
220
+
221
+ #### `mockTransaction()`
222
+
223
+ Creates a mock for the transaction utility. By default, executes the callback with the provided Knex instance (no actual
224
+ transaction wrapper).
225
+
226
+ **Returns:** Mock module object with `transaction` function
227
+
228
+ **Example:**
229
+
230
+ ```typescript
231
+ vi.mock('../utils/transaction.js', async () => {
232
+ const { mockTransaction } = await import('../__mocks__/database.js');
233
+ return mockTransaction();
234
+ });
235
+
236
+ // Transaction callback is executed immediately with the same knex instance
237
+ await transaction(db, async (trx) => {
238
+ // trx === db in tests
239
+ await trx('users').insert({ name: 'John' });
240
+ });
241
+ ```
242
+
243
+ ---
244
+
245
+ ### cache.ts
246
+
247
+ Provides cache system mocking utilities.
248
+
249
+ #### `mockCache()`
250
+
251
+ Creates a standard cache module mock with getCache, getCacheValue, setCacheValue, and clearSystemCache. Returns both the
252
+ mocks for vi.mock() declarations and spies for testing cache behavior.
253
+
254
+ **Returns:** Object with mock functions and spies
255
+
256
+ - `getCache`: Mock function returning cache object
257
+ - `getCacheValue`: Mock function returning null
258
+ - `setCacheValue`: Mock function returning undefined
259
+ - `clearSystemCache`: Mock function
260
+ - `spies`: Spy functions for testing cache behavior
261
+ - `clearSpy`: Spy for cache.clear()
262
+ - `systemClearSpy`: Spy for systemCache.clear()
263
+ - `getCacheSpy`: Spy for localSchemaCache.get()
264
+ - `setCacheSpy`: Spy for localSchemaCache.set()
265
+ - `mockCacheReturn`: The mock cache object to pass to vi.mocked()
266
+
267
+ **Example:**
268
+
269
+ ```typescript
270
+ // Standard usage for vi.mock()
271
+ vi.mock('../cache.js', async () => {
272
+ const { mockCache } = await import('../__mocks__/cache.js');
273
+ return mockCache();
274
+ });
275
+
276
+ // Testing cache clearing with spies
277
+ import { getCache } from '../cache.js';
278
+ import { mockCache } from '../__mocks__/cache.js';
279
+
280
+ test('should clear cache after update', async () => {
281
+ const { spies } = mockCache();
282
+ vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);
283
+
284
+ const service = new YourService({ knex: db, schema });
285
+ await service.updateOne('1', { name: 'Updated' });
286
+
287
+ expect(spies.clearSpy).toHaveBeenCalled();
288
+ });
289
+ ```
290
+
291
+ ---
292
+
293
+ ### schema.ts
294
+
295
+ Provides schema inspector mocking utilities for the `@directus/schema` package.
296
+
297
+ #### `mockSchema()`
298
+
299
+ Creates a standard schema inspector mock with tableInfo, columnInfo, primary, foreignKeys, etc.
300
+
301
+ **Returns:** Mock module object for `vi.mock()`
302
+
303
+ **Example:**
304
+
305
+ ```typescript
306
+ // Standard usage
307
+ vi.mock('@directus/schema', async () => {
308
+ const { mockSchema } = await import('../__mocks__/schema.js');
309
+ return mockSchema();
310
+ });
311
+
312
+ // Dynamically change inspector behavior during tests
313
+ import { createInspector } from '@directus/schema';
314
+ vi.mocked(createInspector).mockReturnValue({
315
+ tableInfo: vi.fn().mockResolvedValue([{ name: 'users' }, { name: 'posts' }]),
316
+ columnInfo: vi.fn().mockResolvedValue([
317
+ { table: 'users', name: 'id', data_type: 'integer', is_nullable: false },
318
+ { table: 'users', name: 'name', data_type: 'varchar', is_nullable: true },
319
+ ]),
320
+ primary: vi.fn().mockResolvedValue('id'),
321
+ foreignKeys: vi.fn().mockResolvedValue([]),
322
+ withSchema: vi.fn().mockReturnThis(),
323
+ } as any);
324
+ ```
325
+
326
+ ---
327
+
328
+ ### emitter.ts
329
+
330
+ Provides event emitter mocking utilities.
331
+
332
+ #### `mockEmitter()`
333
+
334
+ Creates a standard emitter mock with emitAction, emitFilter, emitInit, and event listener methods.
335
+
336
+ **Returns:** Mock module object for `vi.mock()`
337
+
338
+ **Example:**
339
+
340
+ ```typescript
341
+ // Standard usage
342
+ vi.mock('../emitter.js', async () => {
343
+ const { mockEmitter } = await import('../__mocks__/emitter.js');
344
+ return mockEmitter();
345
+ });
346
+
347
+ // Dynamically change emitter behavior during tests
348
+ import emitter from '../emitter.js';
349
+
350
+ // Mock filter to modify payload
351
+ vi.mocked(emitter.emitFilter).mockResolvedValue({ modified: true });
352
+
353
+ // Verify action was emitted
354
+ await service.createOne(data);
355
+ expect(emitter.emitAction).toHaveBeenCalledWith(
356
+ 'items.create',
357
+ expect.objectContaining({ collection: 'test_collection' }),
358
+ expect.any(Object),
359
+ );
360
+ ```
361
+
362
+ ---
363
+
364
+ ### items-service.ts
365
+
366
+ Provides ItemsService mocking utilities for testing services that depend on ItemsService.
367
+
368
+ #### `mockItemsService()`
369
+
370
+ Creates a standard ItemsService mock with all CRUD methods pre-configured with sensible defaults.
371
+
372
+ **Returns:** Mock module object with `ItemsService` class
373
+
374
+ **Default return values:**
375
+
376
+ - `createOne` → `1`
377
+ - `createMany` → `[1]`
378
+ - `readByQuery` → `[]`
379
+ - `readOne` → `{}`
380
+ - `readMany` → `[]`
381
+ - `updateOne` → `1`
382
+ - `updateMany` → `[1]`
383
+ - `updateByQuery` → `[1]`
384
+ - `deleteOne` → `1`
385
+ - `deleteMany` → `[1]`
386
+ - `deleteByQuery` → `[1]`
387
+
388
+ **Example:**
389
+
390
+ ```typescript
391
+ // Standard usage
392
+ vi.mock('./items.js', async () => {
393
+ const { mockItemsService } = await import('../__mocks__/items-service.js');
394
+ return mockItemsService();
395
+ });
396
+
397
+ // Override specific methods during tests
398
+ import { ItemsService } from './items.js';
399
+
400
+ const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('custom-id');
401
+
402
+ await service.createOne(data);
403
+ expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));
404
+
405
+ // Mock readByQuery to return specific data
406
+ vi.spyOn(ItemsService.prototype, 'readByQuery').mockResolvedValue([
407
+ { collection: 'users', name: 'John' },
408
+ { collection: 'posts', name: 'My Post' },
409
+ ]);
410
+ ```
411
+
412
+ ---
413
+
414
+ ### fields-service.ts
415
+
416
+ Provides FieldsService mocking utilities for testing services that depend on FieldsService (like CollectionsService).
417
+
418
+ #### `mockFieldsService()`
419
+
420
+ Creates a standard FieldsService mock with common methods pre-configured.
421
+
422
+ **Returns:** Mock module object with `FieldsService` class
423
+
424
+ **Mocked methods:**
425
+
426
+ - `addColumnToTable` → no-op function
427
+ - `addColumnIndex` → resolves to undefined
428
+ - `deleteField` → resolves to undefined
429
+ - `createField` → resolves to undefined
430
+ - `updateField` → resolves to 'field'
431
+
432
+ **Example:**
433
+
434
+ ```typescript
435
+ // Standard usage in CollectionsService tests
436
+ vi.mock('./fields.js', async () => {
437
+ const { mockFieldsService } = await import('../__mocks__/fields-service.js');
438
+ return mockFieldsService();
439
+ });
440
+
441
+ // Override specific methods during tests
442
+ import { FieldsService } from './fields.js';
443
+
444
+ const addColumnIndexSpy = vi.spyOn(FieldsService.prototype, 'addColumnIndex')
445
+ .mockResolvedValue();
446
+
447
+ await service.createOne({ collection: 'test', fields: [...] });
448
+ expect(addColumnIndexSpy).toHaveBeenCalled();
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Common Patterns
454
+
455
+ ### Full Service Test Setup
456
+
457
+ ```typescript
458
+ import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
459
+ import { SchemaBuilder } from '@directus/schema-builder';
460
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
461
+
462
+ // Mock all dependencies (before imports)
463
+ vi.mock('../../src/database/index', async () => {
464
+ const { mockDatabase } = await import('../__mocks__/database.js');
465
+ return mockDatabase();
466
+ });
467
+
468
+ vi.mock('@directus/schema', async () => {
469
+ const { mockSchema } = await import('../__mocks__/schema.js');
470
+ return mockSchema();
471
+ });
472
+
473
+ vi.mock('../cache.js', async () => {
474
+ const { mockCache } = await import('../__mocks__/cache.js');
475
+ return mockCache();
476
+ });
477
+
478
+ vi.mock('../emitter.js', async () => {
479
+ const { mockEmitter } = await import('../__mocks__/emitter.js');
480
+ return mockEmitter();
481
+ });
482
+
483
+ vi.mock('./items.js', async () => {
484
+ const { mockItemsService } = await import('../__mocks__/items-service.js');
485
+ return mockItemsService();
486
+ });
487
+
488
+ vi.mock('../utils/transaction.js', async () => {
489
+ const { mockTransaction } = await import('../__mocks__/database.js');
490
+ return mockTransaction();
491
+ });
492
+
493
+ // Import after mocks
494
+ import { YourService } from './your-service.js';
495
+
496
+ // Define test schema
497
+ const schema = new SchemaBuilder()
498
+ .collection('users', (c) => {
499
+ c.field('id').integer().primary();
500
+ c.field('name').string();
501
+ })
502
+ .build();
503
+
504
+ describe('Integration Tests', () => {
505
+ const { db, tracker, mockSchemaBuilder } = createMockKnex();
506
+
507
+ afterEach(() => {
508
+ resetKnexMocks(tracker, mockSchemaBuilder);
509
+ });
510
+
511
+ describe('Services / YourService', () => {
512
+ test('should create a user', async () => {
513
+ tracker.on.select('users').response([]);
514
+ tracker.on.insert('users').response([1]);
515
+
516
+ const service = new YourService({ knex: db, schema });
517
+ const result = await service.createOne({ name: 'John' });
518
+
519
+ expect(result).toBe(1);
520
+ });
521
+ });
522
+ });
523
+ ```
524
+
525
+ ### Testing Schema Operations
526
+
527
+ ```typescript
528
+ import { mockCreateTable, mockAlterTable, createMockTableBuilder } from '../__mocks__/knex.js';
529
+
530
+ test('should create table with correct schema', async () => {
531
+ const { db, mockSchemaBuilder } = createMockKnex();
532
+ const service = new YourService({ knex: db, schema });
533
+
534
+ await service.createCollection('users');
535
+
536
+ expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));
537
+ });
538
+
539
+ test('should alter table to add column', async () => {
540
+ const { db } = createMockKnex();
541
+ const alterTableSpy = mockAlterTable();
542
+ db.schema.alterTable = alterTableSpy as any;
543
+
544
+ const service = new YourService({ knex: db, schema });
545
+ await service.addField('users', { field: 'email', type: 'string' });
546
+
547
+ expect(alterTableSpy).toHaveBeenCalledWith('users', expect.any(Function));
548
+ });
549
+ ```
550
+
551
+ ### Testing Cache Clearing
552
+
553
+ ```typescript
554
+ import { getCache } from '../cache.js';
555
+ import { mockCache } from '../__mocks__/cache.js';
556
+
557
+ test('should clear cache after update', async () => {
558
+ const { spies } = mockCache();
559
+ vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);
560
+
561
+ const service = new YourService({ knex: db, schema });
562
+ await service.updateOne('1', { name: 'Updated' });
563
+
564
+ expect(spies.clearSpy).toHaveBeenCalled();
565
+ });
566
+ ```
567
+
568
+ ### Testing with Accountability
569
+
570
+ ```typescript
571
+ test('should allow admin to create collection', async () => {
572
+ const service = new CollectionsService({
573
+ knex: db,
574
+ schema,
575
+ accountability: { role: 'admin', admin: true } as Accountability,
576
+ });
577
+
578
+ await expect(service.createOne({ collection: 'test' })).resolves.toBeDefined();
579
+ });
580
+
581
+ test('should deny non-admin from creating collection', async () => {
582
+ const service = new CollectionsService({
583
+ knex: db,
584
+ schema,
585
+ accountability: { role: 'editor', admin: false } as Accountability,
586
+ });
587
+
588
+ await expect(service.createOne({ collection: 'test' })).rejects.toThrow(ForbiddenError);
589
+ });
590
+
591
+ test('should read column info', async () => {
592
+ const mockColumns = [
593
+ createMockColumn({ table: 'users', name: 'id', data_type: 'integer', is_primary_key: true }),
594
+ createMockColumn({ table: 'users', name: 'email', data_type: 'varchar', max_length: 255 }),
595
+ ];
596
+
597
+ service.schemaInspector.columnInfo = vi.fn().mockResolvedValue(mockColumns);
598
+ const result = await service.columnInfo('users');
599
+
600
+ expect(result).toEqual(mockColumns);
601
+ });
602
+ ```
603
+
604
+ ### Testing with System Collection Mocks
605
+
606
+ ```typescript
607
+ import { setupSystemCollectionMocks } from '../__mocks__/knex.js';
608
+
609
+ describe('Service Tests', () => {
610
+ const { db, tracker, mockSchemaBuilder } = createMockKnex();
611
+
612
+ beforeEach(() => {
613
+ // Automatically mock all CRUD operations for system collections
614
+ setupSystemCollectionMocks(tracker);
615
+ });
616
+
617
+ test('should query directus_fields', async () => {
618
+ // Override the default empty response for specific tests
619
+ tracker.on.select('directus_fields').response([{ id: 1, collection: 'users', field: 'name' }]);
620
+
621
+ const service = new YourService({ knex: db, schema });
622
+ const fields = await service.getFields('users');
623
+
624
+ expect(fields).toHaveLength(1);
625
+ });
626
+ });
627
+ ```
628
+
629
+ ### Mocking Additional Dependencies
630
+
631
+ ```typescript
632
+ // Mock environment variables
633
+ vi.mock('@directus/env', () => ({
634
+ useEnv: vi.fn().mockReturnValue({
635
+ CACHE_SCHEMA: true,
636
+ DB_CLIENT: 'postgres',
637
+ }),
638
+ }));
639
+
640
+ // Mock getSchema utility
641
+ vi.mock('../utils/get-schema.js', () => ({
642
+ getSchema: vi.fn(),
643
+ }));
644
+
645
+ // Mock permissions utilities
646
+ vi.mock('../permissions/lib/fetch-permissions.js', () => ({
647
+ fetchPermissions: vi.fn().mockResolvedValue([
648
+ {
649
+ collection: 'test_collection',
650
+ fields: ['*'],
651
+ action: 'read',
652
+ },
653
+ ]),
654
+ }));
655
+
656
+ // Use in tests
657
+ import { getSchema } from '../utils/get-schema.js';
658
+ vi.mocked(getSchema).mockResolvedValue(schema);
659
+ ```
660
+
661
+ ---
662
+
663
+ ## Examples
664
+
665
+ See these files for complete examples:
666
+
667
+ - [collections.test.ts](../services/collections.test.ts) - Full service test with schema operations
668
+ - [fields.test.ts](../services/fields.test.ts) - Complex service test with field management
669
+
670
+ ---
671
+
672
+ ## Best Practices
673
+
674
+ ### Mock Declaration Order
675
+
676
+ Always declare `vi.mock()` calls **before** importing the modules they mock:
677
+
678
+ ```typescript
679
+ // ✅ Correct - mocks first
680
+ vi.mock('../cache.js', async () => {
681
+ const { mockCache } = await import('../__mocks__/cache.js');
682
+ return mockCache();
683
+ });
684
+
685
+ import { YourService } from './your-service.js';
686
+
687
+ // ❌ Wrong - imports before mocks
688
+ import { YourService } from './your-service.js';
689
+
690
+ vi.mock('../cache.js', async () => {
691
+ const { mockCache } = await import('../__mocks__/cache.js');
692
+ return mockCache();
693
+ });
694
+ ```
695
+
696
+ ### Resetting Mocks Between Tests
697
+
698
+ Always reset mocks in `afterEach` to prevent test interference:
699
+
700
+ ```typescript
701
+ afterEach(() => {
702
+ resetKnexMocks(tracker, mockSchemaBuilder);
703
+ vi.clearAllMocks(); // Clear any additional spies
704
+ });
705
+ ```
706
+
707
+ ### Tracker Response Patterns
708
+
709
+ Set up tracker responses for every database query your test will execute:
710
+
711
+ ```typescript
712
+ test('should create and read item', async () => {
713
+ // Mock all queries that will run
714
+ tracker.on.select('users').response([]); // Check if exists
715
+ tracker.on.insert('users').response([1]); // Create
716
+ tracker.on.select('users').response([{ id: 1, name: 'John' }]); // Read back
717
+
718
+ // Your test code...
719
+ });
720
+ ```
721
+
722
+ ### Using TypeScript with Mocks
723
+
724
+ Add type assertions when needed to satisfy TypeScript:
725
+
726
+ ```typescript
727
+ vi.mocked(getCache).mockReturnValue({
728
+ cache: { clear: vi.fn() },
729
+ systemCache: { clear: vi.fn() },
730
+ localSchemaCache: { get: vi.fn(), set: vi.fn() },
731
+ lockCache: undefined,
732
+ } as any);
733
+ ```
734
+
735
+ ### Spying on Service Methods
736
+
737
+ Use `vi.spyOn` to override specific methods while keeping the rest of the service intact:
738
+
739
+ ```typescript
740
+ import { ItemsService } from './items.js';
741
+
742
+ const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('new-id');
743
+
744
+ // Test your code
745
+ expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));
746
+ ```
747
+
748
+ ---
749
+
750
+ ## Contributing
751
+
752
+ When adding new mock utilities:
753
+
754
+ 1. Create or update the appropriate mock file
755
+ 2. Add comprehensive JSDoc comments with examples
756
+ 3. Document the utility in this README
757
+ 4. Export all functions from the module
758
+ 5. Use TypeScript for proper typing
759
+ 6. Include return type documentation
760
+ 7. Test the mock with actual service tests