@tsed/directus-sdk 8.19.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.
@@ -0,0 +1,184 @@
1
+ import { getCache, getCacheValue, setCacheValue } from "@directus/api/cache";
2
+ import { classOf } from "@tsed/core/utils/classOf.js";
3
+ import { nameOf } from "@tsed/core/utils/nameOf.js";
4
+ import { bindIntercept, injectable } from "@tsed/di";
5
+ import { snakeCase } from "lodash";
6
+ /**
7
+ * Default cache key generator that serializes method arguments into a cache key.
8
+ *
9
+ * Creates a JSON string of the arguments, or an empty string if no arguments.
10
+ * This ensures unique cache keys for different argument combinations.
11
+ *
12
+ * @param args - The method arguments to serialize
13
+ * @returns A string representation of the arguments, prefixed with ':'
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * defaultKeyGenerator("user", 123)
18
+ * // Returns: ':["user",123]'
19
+ *
20
+ * defaultKeyGenerator()
21
+ * // Returns: ':'
22
+ * ```
23
+ */
24
+ function defaultKeyGenerator(...args) {
25
+ return args.length > 0 ? JSON.stringify(args) : "";
26
+ }
27
+ /**
28
+ * Interceptor that provides caching functionality using Directus cache infrastructure.
29
+ *
30
+ * This interceptor wraps method calls to cache their results based on method arguments.
31
+ * It integrates with Directus's built-in caching system and supports both system cache
32
+ * and regular cache.
33
+ *
34
+ * Features:
35
+ * - Automatic cache key generation based on class name, method name, and arguments
36
+ * - Configurable TTL (time to live) for cached values
37
+ * - Custom key generators for complex caching strategies
38
+ * - Support for both system cache and regular cache
39
+ * - Proper handling of falsy values (null, false, 0, empty string)
40
+ *
41
+ * ### Direct usage with bindIntercept
42
+ *
43
+ * ```ts
44
+ * import {DirectusCacheInterceptor, useDirectusCache} from "@tsed/directus-sdk";
45
+ * import {injectable} from "@tsed/di";
46
+ *
47
+ * export class MyService {
48
+ * search(query: string) {
49
+ * // Expensive operation
50
+ * return this.performSearch(query);
51
+ * }
52
+ * }
53
+ *
54
+ * // Apply cache programmatically
55
+ * injectable(MyService);
56
+ * useDirectusCache(MyService, "search", { ttl: 60000 });
57
+ * ```
58
+ * @remarks
59
+ * This interceptor is typically used via the `@Cache` decorator rather than directly.
60
+ * The cache key format is: `{namespace}:{keyGenerator(...args)}`
61
+ * where namespace defaults to `snake_case_class_name:methodName`
62
+ *
63
+ * @see {@link Cache} decorator for the recommended usage
64
+ * @see {@link useDirectusCache} for programmatic cache binding
65
+ */
66
+ export class DirectusCacheInterceptor {
67
+ /**
68
+ * Intercepts a method call to provide caching functionality.
69
+ *
70
+ * The interception flow:
71
+ * 1. Extract cache options from the context
72
+ * 2. Generate a cache key based on class name, method name, and arguments
73
+ * 3. Check if a cached value exists
74
+ * 4. If cached, return the cached value (skipping method execution)
75
+ * 5. If not cached, execute the method
76
+ * 6. Store the result in cache with the specified TTL
77
+ * 7. Return the result
78
+ *
79
+ * ### Cache key generation
80
+ *
81
+ * ```ts
82
+ * // For class MyService with method search("query")
83
+ * // Default namespace: "my_service:search"
84
+ * // Full cache key: "my_service:search:["query"]"
85
+ * ```
86
+ * @param context - The interceptor context containing method information and options
87
+ *
88
+ * @returns The cached or newly computed result of the method call
89
+ *
90
+ * @remarks
91
+ * - The cache check uses strict inequality (`!== undefined`) to allow caching falsy values
92
+ * - Cache keys are automatically namespaced to avoid collisions between different methods
93
+ * - The system cache is used by default, which persists across requests
94
+ */
95
+ async intercept(context) {
96
+ const { ttl = 900000, // 15 minutes default
97
+ keyGenerator = defaultKeyGenerator, namespace = snakeCase(nameOf(context.target)) + ":" + String(context.propertyKey), useSystemCache = true } = context.options || {};
98
+ // Get appropriate cache instance
99
+ const cache = getCache();
100
+ const cacheInstance = useSystemCache ? cache.systemCache : cache.cache;
101
+ // Generate cache key
102
+ const cacheKey = `${namespace}:${keyGenerator(...context.args)}`;
103
+ // Try to get from cache
104
+ const cachedValue = await getCacheValue(cacheInstance, cacheKey);
105
+ if (cachedValue !== undefined) {
106
+ return cachedValue;
107
+ }
108
+ // Execute function and cache result
109
+ const result = await context.next();
110
+ await setCacheValue(cacheInstance, cacheKey, result, ttl);
111
+ return result;
112
+ }
113
+ }
114
+ injectable(DirectusCacheInterceptor);
115
+ /**
116
+ * Programmatically applies cache interception to a method using Directus cache infrastructure.
117
+ *
118
+ * This function allows you to add caching to a method after the class is defined,
119
+ * which is useful when you can't use decorators or need dynamic cache configuration.
120
+ *
121
+ * @example Basic usage
122
+ *
123
+ * ```ts
124
+ * import {injectable} from "@tsed/di";
125
+ * import {useDirectusCache} from "@tsed/directus-sdk";
126
+ *
127
+ * export class JiraIssueClient {
128
+ * search(query: string) {
129
+ * // Expensive search operation
130
+ * return this.performSearch(query);
131
+ * }
132
+ * }
133
+ *
134
+ * injectable(JiraIssueClient)
135
+ *
136
+ * // Apply cache after class definition
137
+ * useDirectusCache(JiraIssueClient, "search", { ttl: 900000 });
138
+ * ```
139
+ *
140
+ * ### With custom options
141
+ * ```ts
142
+ *
143
+ * useDirectusCache(MyService, "fetchData", {
144
+ * ttl: 60000,
145
+ * keyGenerator: (id: string) => `data:${id}`,
146
+ * namespace: "my_service:data",
147
+ * useSystemCache: false
148
+ * });
149
+ * ```
150
+ *
151
+ * ### Applying cache to multiple methods
152
+ *
153
+ * ```ts
154
+ * import {injectable} from "@tsed/di";
155
+ * import {useDirectusCache} from "@tsed/directus-sdk";
156
+ *
157
+ * export class ApiClient {
158
+ * getUser(id: string) { }
159
+ * getPosts() { }
160
+ * }
161
+ * injectable(ApiClient);
162
+ *
163
+ * useDirectusCache(ApiClient, "getUser", { ttl: 300000 });
164
+ * useDirectusCache(ApiClient, "getPosts", { ttl: 60000 });
165
+ * ```
166
+ *
167
+ * @template K - The class type
168
+ * @template Props - The property key type (method name)
169
+ *
170
+ * @param token - The class constructor to apply caching to
171
+ * @param propertyKey - The name of the method to cache
172
+ * @param opts - Cache configuration options
173
+ *
174
+ * @remarks
175
+ * - Must be called after the class is marked as `@Injectable()` or `injectable()`
176
+ * - The class must be registered in the DI container
177
+ * - Prefer using the `@Cache` decorator when possible for better readability
178
+ *
179
+ * @see {@link Cache} decorator for the declarative approach
180
+ * @see {@link DirectusCacheInterceptor} for implementation details
181
+ */
182
+ export function useDirectusCache(token, propertyKey, opts) {
183
+ bindIntercept(classOf(token), propertyKey, DirectusCacheInterceptor, opts);
184
+ }
@@ -0,0 +1,162 @@
1
+ import { context, injectable } from "@tsed/di";
2
+ /**
3
+ * Service to manage and access Directus context within Ts.ED dependency injection container.
4
+ *
5
+ * This service provides a bridge between Directus extensions and Ts.ED's DI system,
6
+ * allowing you to access the Directus context and its services from anywhere in your code.
7
+ *
8
+ * The context is automatically set by `wrapEndpoint`, `wrapOperation`, and `defineHook`
9
+ * wrappers, making it available through the DI container.
10
+ *
11
+ * ### Accessing context in a custom service
12
+ *
13
+ * ```ts
14
+ * import {Injectable} from "@tsed/di";
15
+ * import {DirectusContextService} from "@tsed/directus-sdk";
16
+ *
17
+ * @Injectable()
18
+ * export class MyService {
19
+ * directusContext = inject(DirectusContextService);
20
+ *
21
+ * async doSomething() {
22
+ * const context = this.directusContext.get();
23
+ * const schema = await context.getSchema();
24
+ * // Use schema...
25
+ * }
26
+ * }
27
+ * ```
28
+ *
29
+ * ### Using inject() to get the service
30
+ *
31
+ * ```ts
32
+ * import {inject} from "@tsed/di";
33
+ * import {DirectusContextService} from "@tsed/directus-sdk";
34
+ *
35
+ * const contextService = inject(DirectusContextService);
36
+ * const context = contextService.get();
37
+ *```
38
+ *
39
+ * @remarks
40
+ * This service is automatically registered as injectable and can be accessed via `inject()`.
41
+ */
42
+ export class DirectusContextService {
43
+ /**
44
+ * Sets the Directus context in the current DI context.
45
+ *
46
+ * This method is typically called internally by the wrapper functions
47
+ * (`wrapEndpoint`, `wrapOperation`, `defineHook`) and should not be
48
+ * called manually in most cases.
49
+ *
50
+ * @param ctx - The Directus extension context (endpoint, operation, or hook)
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const contextService = inject(DirectusContextService);
55
+ * contextService.set(directusContext);
56
+ * ```
57
+ */
58
+ set(ctx) {
59
+ context().set("DIRECTUS_CONTEXT", ctx);
60
+ }
61
+ /**
62
+ * Retrieves the Directus context from the current DI context.
63
+ *
64
+ * Returns the context that was previously set by the wrapper functions.
65
+ * This allows you to access Directus services, schema, logger, and other
66
+ * context-specific functionality.
67
+ *
68
+ * ### Accessing context properties
69
+ *
70
+ * ```ts
71
+ * const contextService = inject(DirectusContextService);
72
+ * const context = contextService.get();
73
+ *
74
+ * // Access logger
75
+ * context.logger.info("Message");
76
+ *
77
+ * // Access schema
78
+ * const schema = await context.getSchema();
79
+ *
80
+ * // Access services
81
+ * const itemsService = new context.services.ItemsService("users");
82
+ * ```
83
+ *
84
+ * @returns The Directus extension context, or undefined if not set
85
+ *
86
+ */
87
+ get() {
88
+ return context().get("DIRECTUS_CONTEXT");
89
+ }
90
+ /**
91
+ * Creates and returns an ItemsService instance for the specified collection.
92
+ *
93
+ * This is a convenience method that:
94
+ *
95
+ * 1. Retrieves the Directus context
96
+ * 2. Fetches the database schema
97
+ * 3. Creates an ItemsService with the schema and provided options
98
+ *
99
+ * The ItemsService allows you to perform CRUD operations on Directus collections.
100
+ *
101
+ * ### Basic usage
102
+ *
103
+ * ```ts
104
+ * const contextService = inject(DirectusContextService);
105
+ * const usersService = await contextService.getItemsService("users");
106
+ *
107
+ * // Read all users
108
+ * const users = await usersService.readByQuery({ limit: -1 });
109
+ *
110
+ * // Create a user
111
+ * const userId = await usersService.createOne({ name: "John", email: "john@example.com" });
112
+ * ```
113
+ *
114
+ * ### With typed collection
115
+ *
116
+ * ```ts
117
+ * type User = Item & {
118
+ * id: string;
119
+ * name: string;
120
+ * email: string;
121
+ * };
122
+ *
123
+ * const contextService = inject(DirectusContextService);
124
+ * const usersService = await contextService.getItemsService<User, "users">("users");
125
+ *
126
+ * // Now fully typed
127
+ * const users: User[] = await usersService.readByQuery({ limit: -1 });
128
+ * ```
129
+ *
130
+ * ### With accountability options
131
+ *
132
+ * ```ts
133
+ * const contextService = inject(DirectusContextService);
134
+ * const itemsService = await contextService.getItemsService("articles", {
135
+ * accountability: {
136
+ * user: "user-id",
137
+ * role: "admin"
138
+ * }
139
+ * });
140
+ * ```
141
+ *
142
+ * @template T - The item type extending Directus Item
143
+ * @template Collection - The collection name as a string literal type
144
+ * @param collection - The name of the collection to access
145
+ * @param options - Additional options for the ItemsService (e.g., accountability, knex instance)
146
+ * @returns A promise resolving to an ItemsService instance for the collection
147
+ * @throws {Error} If no Directus context is available (called outside extension context)
148
+ * @see {@link DirectusItemsRepository} for a more structured way to work with collections
149
+ */
150
+ async getItemsService(collection, options = {}) {
151
+ const context = this.get();
152
+ if (context) {
153
+ const schema = await context.getSchema();
154
+ return new context.services.ItemsService(collection, {
155
+ ...options,
156
+ schema
157
+ });
158
+ }
159
+ throw new Error("No directus context available");
160
+ }
161
+ }
162
+ injectable(DirectusContextService);
@@ -0,0 +1,240 @@
1
+ import { inject } from "@tsed/di";
2
+ import { DirectusContextService } from "./DirectusContextService.js";
3
+ /**
4
+ * Abstract base class for creating typed repository patterns with Directus collections.
5
+ *
6
+ * This class provides a structured way to interact with Directus collections by:
7
+ * - Automatically retrieving the Directus context via dependency injection
8
+ * - Providing common CRUD operations (create, update, listAll)
9
+ * - Supporting custom methods through inheritance
10
+ * - Ensuring type safety with TypeScript generics
11
+ *
12
+ * Extend this class to create collection-specific repositories with custom business logic.
13
+ *
14
+ * ### Basic repository with custom methods
15
+ * ```ts
16
+ * import {injectable} from "@tsed/di";
17
+ * import {DirectusItemsRepository} from "@tsed/directus-sdk";
18
+ * import type {Item} from "@directus/types";
19
+ *
20
+ * type Release = Item & {
21
+ * id: string;
22
+ * title: string;
23
+ * status: "draft" | "published";
24
+ * publishedAt?: string;
25
+ * };
26
+ *
27
+ * export class ReleasesRepository extends DirectusItemsRepository<Release, "releases"> {
28
+ * protected collection = "releases" as const;
29
+ *
30
+ * async getPublishedReleases() {
31
+ * const collection = await this.getCollection();
32
+ *
33
+ * return collection.readByQuery({
34
+ * filter: {
35
+ * status: {
36
+ * _eq: "published"
37
+ * }
38
+ * },
39
+ * sort: ["-publishedAt"]
40
+ * });
41
+ * }
42
+ *
43
+ * async publishRelease(id: string) {
44
+ * return this.update({
45
+ * id,
46
+ * status: "published",
47
+ * publishedAt: new Date().toISOString()
48
+ * });
49
+ * }
50
+ * }
51
+ * injectable(ReleasesRepository)
52
+ * ```
53
+ *
54
+ * ### Repository with complex queries
55
+ *
56
+ * ```ts
57
+ * import {injectable} from "@tsed/di";
58
+ * import {DirectusItemsRepository} from "@tsed/directus-sdk";
59
+ *
60
+ * export class ArticlesRepository extends DirectusItemsRepository<Article, "articles"> {
61
+ * protected collection = "articles" as const;
62
+ *
63
+ * async findByAuthor(authorId: string) {
64
+ * const collection = await this.getCollection();
65
+ *
66
+ * return collection.readByQuery({
67
+ * filter: {
68
+ * author: {
69
+ * _eq: authorId
70
+ * }
71
+ * }
72
+ * });
73
+ * }
74
+ *
75
+ * async searchByTitle(query: string) {
76
+ * const collection = await this.getCollection();
77
+ *
78
+ * return collection.readByQuery({
79
+ * filter: {
80
+ * title: {
81
+ * _contains: query
82
+ * }
83
+ * }
84
+ * });
85
+ * }
86
+ * }
87
+ * injectable(ArticlesRepository);
88
+ * ```
89
+ *
90
+ * ### Using repository in an endpoint
91
+ *
92
+ * ```ts
93
+ * import {defineEndpoint} from "@tsed/directus-sdk";
94
+ * import {inject} from "@tsed/di";
95
+ *
96
+ * export default defineEndpoint({
97
+ * id: "releases-api",
98
+ * handler: (router) => {
99
+ * const releasesRepo = inject(ReleasesRepository);
100
+ *
101
+ * router.get("/releases", async (req, res) => {
102
+ * const releases = await releasesRepo.getPublishedReleases();
103
+ * res.json(releases);
104
+ * });
105
+ *
106
+ * router.post("/releases/:id/publish", async (req, res) => {
107
+ * const release = await releasesRepo.publishRelease(req.params.id);
108
+ * res.json(release);
109
+ * });
110
+ * }
111
+ * });
112
+ * ```
113
+ *
114
+ * @template T - The item type extending Directus Item interface
115
+ * @template Collection - The collection name as a string literal type
116
+ *
117
+ * @remarks
118
+ * - The extending class must define the `collection` property
119
+ * - Don't forget to mark the extending class with `@Injectable()` or `injectable()`
120
+ * - Access to Directus context is automatically handled through `DirectusContextService`
121
+ *
122
+ * @see {@link DirectusContextService} for lower-level access to Directus context
123
+ */
124
+ export class DirectusItemsRepository {
125
+ constructor() {
126
+ this.contextService = inject(DirectusContextService);
127
+ }
128
+ /**
129
+ * Retrieves the Directus ItemsService for the configured collection.
130
+ *
131
+ * This method provides access to the underlying Directus ItemsService,
132
+ * which offers full CRUD capabilities and advanced query features.
133
+ *
134
+ * ```ts
135
+ * const collection = await this.getCollection();
136
+ * const items = await collection.readByQuery({
137
+ * filter: { status: { _eq: "active" } },
138
+ * limit: 10,
139
+ * offset: 0
140
+ * });
141
+ * ```
142
+ *
143
+ * @returns A promise resolving to the ItemsService for the collection
144
+ *
145
+ */
146
+ getCollection() {
147
+ return this.contextService.getItemsService(this.collection);
148
+ }
149
+ /**
150
+ * Creates a new item in the collection and returns the created item.
151
+ *
152
+ * ```ts
153
+ * const newRelease = await releasesRepo.create({
154
+ * title: "Version 1.0",
155
+ * status: "draft"
156
+ * });
157
+ *
158
+ * console.log(newRelease.id); // Auto-generated ID
159
+ * ```
160
+ *
161
+ * ### With mutation options
162
+ *
163
+ * ```ts
164
+ * const item = await repo.create(data, {
165
+ * emitEvents: false,
166
+ * bypassLimits: true
167
+ * });
168
+ * ```
169
+ *
170
+ * @param data - Partial item data to create
171
+ * @param opts - Optional mutation options (e.g., emitEvents, bypassLimits)
172
+ *
173
+ * @returns A promise resolving to the created item with all fields
174
+ */
175
+ async create(data, opts) {
176
+ const collection = await this.getCollection();
177
+ const key = await collection.createOne(data, opts);
178
+ return collection.readOne(key);
179
+ }
180
+ /**
181
+ * Updates an existing item in the collection and returns the updated item.
182
+ *
183
+ * ```ts
184
+ * const updated = await releasesRepo.update({
185
+ * id: "release-123",
186
+ * status: "published"
187
+ * });
188
+ * ```
189
+ *
190
+ * ### With mutation options
191
+ *
192
+ * ```ts
193
+ * const updated = await repo.update(
194
+ * { id: "123", title: "New Title" },
195
+ * { emitEvents: true }
196
+ * );
197
+ * ```
198
+ *
199
+ * @param data - Partial item data including the required `id` field
200
+ * @param opts - Optional mutation options (e.g., emitEvents, bypassLimits)
201
+ *
202
+ * @returns A promise resolving to the updated item
203
+ */
204
+ async update(data, opts) {
205
+ const collection = await this.getCollection();
206
+ const key = await collection.updateOne(data.id, data, opts);
207
+ return collection.readOne(key);
208
+ }
209
+ /**
210
+ * Retrieves all items from the collection without pagination.
211
+ *
212
+ * ```ts
213
+ * const allReleases = await releasesRepo.listAll();
214
+ * console.log(`Total releases: ${allReleases.length}`);
215
+ * ```
216
+ *
217
+ * ### For large collections, consider custom pagination
218
+ *
219
+ * ```ts
220
+ * async getPaginatedItems(page: number, pageSize: number) {
221
+ * const collection = await this.getCollection();
222
+ * return collection.readByQuery({
223
+ * limit: pageSize,
224
+ * offset: (page - 1) * pageSize
225
+ * });
226
+ * }
227
+ * ```
228
+ *
229
+ * @returns A promise resolving to an array of all items in the collection
230
+ *
231
+ * @remarks
232
+ * This method uses `limit: -1` to retrieve all items. Use with caution on
233
+ * large collections as it may impact performance.
234
+ *
235
+ */
236
+ async listAll() {
237
+ const service = await this.getCollection();
238
+ return (await service.readByQuery({ limit: -1 }));
239
+ }
240
+ }
@@ -0,0 +1 @@
1
+ import "@tsed/logger-connect";
@@ -0,0 +1 @@
1
+ import "./attach-logger.js";
@@ -0,0 +1,57 @@
1
+ import { type DirectusCacheOptions } from "../services/DirectusCacheInterceptor.js";
2
+ /**
3
+ * Decorator to cache method results using Directus cache system.
4
+ *
5
+ * This decorator uses the Directus cache infrastructure (system cache by default) to store
6
+ * method results. Cached values are retrieved on subsequent calls with the same arguments,
7
+ * avoiding redundant computations or API calls.
8
+ *
9
+ * ### Basic usage
10
+ *
11
+ * ```ts
12
+ * import {Injectable} from "@tsed/di";
13
+ * import {Cache} from "@tsed/directus-sdk";
14
+ *
15
+ * @Injectable()
16
+ * export class JiraIssueClient {
17
+ * // Cache search results for 15 minutes (default TTL)
18
+ * @Cache({ ttl: 900000 })
19
+ * search(query: string) {
20
+ * // Expensive API call
21
+ * return this.fetchFromJira(query);
22
+ * }
23
+ *
24
+ * // Custom cache key generator
25
+ * @Cache({
26
+ * ttl: 60000,
27
+ * keyGenerator: (userId: string) => `user:${userId}`
28
+ * })
29
+ * getUserById(userId: string) {
30
+ * return this.fetchUser(userId);
31
+ * }
32
+ *
33
+ * // Use regular cache instead of system cache
34
+ * @Cache({
35
+ * ttl: 300000,
36
+ * useSystemCache: false,
37
+ * namespace: "custom:namespace"
38
+ * })
39
+ * getCustomData() {
40
+ * return this.fetchCustomData();
41
+ * }
42
+ * }
43
+ * ```
44
+ *
45
+ * @param options - Cache configuration options
46
+ * @param options.ttl - Time to live in milliseconds (default: 900000ms = 15 minutes)
47
+ * @param options.keyGenerator - Custom function to generate cache keys from method arguments
48
+ * @param options.namespace - Custom namespace for cache keys (default: className:methodName)
49
+ * @param options.useSystemCache - Whether to use system cache (true) or regular cache (false). Default: true
50
+ *
51
+ * @returns A method decorator that intercepts method calls and caches results
52
+ *
53
+ * @decorator
54
+ * @see {@link DirectusCacheInterceptor} for implementation details
55
+ * @see {@link DirectusCacheOptions} for available options
56
+ */
57
+ export declare function Cache(options: DirectusCacheOptions): any;