@decocms/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21
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/config-schema.json +553 -0
- package/package.json +5 -14
- package/src/bindings/binder.ts +1 -4
- package/src/bindings/index.ts +0 -33
- package/src/bindings/language-model/utils.ts +0 -91
- package/src/bindings.ts +31 -110
- package/src/client.ts +1 -145
- package/src/cors.ts +140 -0
- package/src/index.ts +84 -167
- package/src/mcp.ts +7 -166
- package/src/proxy.ts +3 -54
- package/src/state.ts +3 -31
- package/src/tools.ts +372 -0
- package/src/wrangler.ts +5 -5
- package/tsconfig.json +1 -1
- package/src/admin.ts +0 -16
- package/src/auth.ts +0 -233
- package/src/bindings/deconfig/helpers.ts +0 -107
- package/src/bindings/deconfig/index.ts +0 -1
- package/src/bindings/deconfig/resources.ts +0 -689
- package/src/bindings/deconfig/types.ts +0 -106
- package/src/bindings/language-model/ai-sdk.ts +0 -90
- package/src/bindings/language-model/index.ts +0 -4
- package/src/bindings/resources/bindings.ts +0 -99
- package/src/bindings/resources/helpers.ts +0 -95
- package/src/bindings/resources/schemas.ts +0 -265
- package/src/bindings/views.ts +0 -14
- package/src/drizzle.ts +0 -201
- package/src/mastra.ts +0 -670
- package/src/resources.ts +0 -168
- package/src/views.ts +0 -26
- package/src/well-known.ts +0 -20
|
@@ -1,689 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DeconfigResources 2.0
|
|
3
|
-
*
|
|
4
|
-
* This module provides file-based resource management using the Resources 2.0 system
|
|
5
|
-
* with standardized `rsc://` URI format and consistent CRUD operations.
|
|
6
|
-
*
|
|
7
|
-
* Key Features:
|
|
8
|
-
* - File-based resource storage in DECONFIG directories
|
|
9
|
-
* - Resources 2.0 standardized schemas and URI format
|
|
10
|
-
* - Type-safe resource definitions with Zod validation
|
|
11
|
-
* - Full CRUD operations with proper error handling
|
|
12
|
-
* - Integration with existing deconfig file system
|
|
13
|
-
* - Support for custom resource schemas and enhancements
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { DefaultEnv } from "../../index.ts";
|
|
17
|
-
import { impl } from "../binder.ts";
|
|
18
|
-
import type { BaseResourceDataSchema } from "../resources/bindings.ts";
|
|
19
|
-
import { createResourceBindings } from "../resources/bindings.ts";
|
|
20
|
-
import { ResourceUriSchema } from "../resources/schemas.ts";
|
|
21
|
-
import {
|
|
22
|
-
ResourcePath,
|
|
23
|
-
ResourceUri,
|
|
24
|
-
getMetadataString,
|
|
25
|
-
normalizeDirectory,
|
|
26
|
-
toAsyncIterator,
|
|
27
|
-
} from "./helpers.ts";
|
|
28
|
-
import type { DeconfigClient, DeconfigResourceOptions } from "./types.ts";
|
|
29
|
-
import { WELL_KNOWN_ORIGINS } from "../../well-known.ts";
|
|
30
|
-
|
|
31
|
-
export type {
|
|
32
|
-
EnhancedResourcesTools,
|
|
33
|
-
ResourcesBinding,
|
|
34
|
-
ResourcesTools,
|
|
35
|
-
} from "./types.ts";
|
|
36
|
-
export type { DeconfigClient, DeconfigResourceOptions };
|
|
37
|
-
|
|
38
|
-
// Error classes - these will be imported from SDK when used there
|
|
39
|
-
export class NotFoundError extends Error {
|
|
40
|
-
constructor(message: string) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "NotFoundError";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class UserInputError extends Error {
|
|
47
|
-
constructor(message: string) {
|
|
48
|
-
super(message);
|
|
49
|
-
this.name = "UserInputError";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const dirOf = (
|
|
54
|
-
options: Pick<
|
|
55
|
-
DeconfigResourceOptions<BaseResourceDataSchema>,
|
|
56
|
-
"directory" | "resourceName"
|
|
57
|
-
>,
|
|
58
|
-
) => {
|
|
59
|
-
return options.directory
|
|
60
|
-
? options.directory
|
|
61
|
-
: `/resources/${options.resourceName}`;
|
|
62
|
-
};
|
|
63
|
-
export const createDeconfigResource = <
|
|
64
|
-
TDataSchema extends BaseResourceDataSchema,
|
|
65
|
-
>(
|
|
66
|
-
options: DeconfigResourceOptions<TDataSchema>,
|
|
67
|
-
) => {
|
|
68
|
-
const {
|
|
69
|
-
resourceName,
|
|
70
|
-
dataSchema,
|
|
71
|
-
enhancements,
|
|
72
|
-
env,
|
|
73
|
-
validate: semanticValidate,
|
|
74
|
-
} = options;
|
|
75
|
-
const deconfig = env.DECONFIG;
|
|
76
|
-
const directory = dirOf(options);
|
|
77
|
-
|
|
78
|
-
// Create resource-specific bindings using the provided data schema
|
|
79
|
-
const resourceBindings = createResourceBindings(resourceName, dataSchema);
|
|
80
|
-
|
|
81
|
-
const tools = impl(resourceBindings, [
|
|
82
|
-
// deco_resource_search
|
|
83
|
-
{
|
|
84
|
-
description:
|
|
85
|
-
enhancements?.[
|
|
86
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_SEARCH` as keyof typeof enhancements
|
|
87
|
-
]?.description ||
|
|
88
|
-
`Search ${resourceName} resources in the DECONFIG directory ${directory}`,
|
|
89
|
-
handler: async ({
|
|
90
|
-
term,
|
|
91
|
-
page = 1,
|
|
92
|
-
pageSize = 10,
|
|
93
|
-
filters,
|
|
94
|
-
sortBy,
|
|
95
|
-
sortOrder,
|
|
96
|
-
}) => {
|
|
97
|
-
const normalizedDir = normalizeDirectory(directory);
|
|
98
|
-
const offset = pageSize !== Infinity ? (page - 1) * pageSize : 0;
|
|
99
|
-
|
|
100
|
-
// List all files in the directory
|
|
101
|
-
const filesList = await deconfig.LIST_FILES({
|
|
102
|
-
prefix: normalizedDir,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Filter files that end with .json
|
|
106
|
-
const allFiles = Object.entries(
|
|
107
|
-
(filesList as { files: Record<string, unknown> }).files,
|
|
108
|
-
)
|
|
109
|
-
.filter(([path]) => path.endsWith(".json"))
|
|
110
|
-
.map(([path, metadata]) => ({
|
|
111
|
-
path,
|
|
112
|
-
resourceId: path
|
|
113
|
-
.replace(`${normalizedDir}/`, "")
|
|
114
|
-
.replace(".json", ""),
|
|
115
|
-
metadata,
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
// Simple search - filter by resource ID, path, title, description, created_by, or updated_by
|
|
119
|
-
let filteredFiles = allFiles;
|
|
120
|
-
if (term) {
|
|
121
|
-
filteredFiles = allFiles.filter(({ resourceId, path, metadata }) => {
|
|
122
|
-
const searchTerm = term.toLowerCase();
|
|
123
|
-
return (
|
|
124
|
-
resourceId.toLowerCase().includes(searchTerm) ||
|
|
125
|
-
path.toLowerCase().includes(searchTerm) ||
|
|
126
|
-
(
|
|
127
|
-
getMetadataString(metadata, "name")?.toLowerCase() ?? ""
|
|
128
|
-
).includes(searchTerm) ||
|
|
129
|
-
(
|
|
130
|
-
getMetadataString(metadata, "description")?.toLowerCase() ?? ""
|
|
131
|
-
).includes(searchTerm) ||
|
|
132
|
-
(
|
|
133
|
-
getMetadataString(metadata, "createdBy")?.toLowerCase() ?? ""
|
|
134
|
-
).includes(searchTerm) ||
|
|
135
|
-
(
|
|
136
|
-
getMetadataString(metadata, "updatedBy")?.toLowerCase() ?? ""
|
|
137
|
-
).includes(searchTerm)
|
|
138
|
-
);
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Apply additional filters if provided
|
|
143
|
-
if (filters) {
|
|
144
|
-
const createdByFilter = filters.created_by as
|
|
145
|
-
| string
|
|
146
|
-
| string[]
|
|
147
|
-
| undefined;
|
|
148
|
-
const updatedByFilter = filters.updated_by as
|
|
149
|
-
| string
|
|
150
|
-
| string[]
|
|
151
|
-
| undefined;
|
|
152
|
-
|
|
153
|
-
if (createdByFilter) {
|
|
154
|
-
const createdBySet = new Set(
|
|
155
|
-
Array.isArray(createdByFilter)
|
|
156
|
-
? createdByFilter.map((v: unknown) => String(v))
|
|
157
|
-
: [String(createdByFilter)],
|
|
158
|
-
);
|
|
159
|
-
filteredFiles = filteredFiles.filter(({ metadata }) => {
|
|
160
|
-
const value = getMetadataString(metadata, "createdBy");
|
|
161
|
-
return value ? createdBySet.has(value) : false;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (updatedByFilter) {
|
|
166
|
-
const updatedBySet = new Set(
|
|
167
|
-
Array.isArray(updatedByFilter)
|
|
168
|
-
? updatedByFilter.map((v: unknown) => String(v))
|
|
169
|
-
: [String(updatedByFilter)],
|
|
170
|
-
);
|
|
171
|
-
filteredFiles = filteredFiles.filter(({ metadata }) => {
|
|
172
|
-
const value = getMetadataString(metadata, "updatedBy");
|
|
173
|
-
return value ? updatedBySet.has(value) : false;
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Sort if specified
|
|
179
|
-
if (sortBy) {
|
|
180
|
-
filteredFiles.sort((a, b) => {
|
|
181
|
-
let aValue: string | number;
|
|
182
|
-
let bValue: string | number;
|
|
183
|
-
|
|
184
|
-
if (sortBy === "resourceId") {
|
|
185
|
-
aValue = a.resourceId;
|
|
186
|
-
bValue = b.resourceId;
|
|
187
|
-
} else if (sortBy === "name") {
|
|
188
|
-
aValue = getMetadataString(a.metadata, "name") || a.resourceId;
|
|
189
|
-
bValue = getMetadataString(b.metadata, "name") || b.resourceId;
|
|
190
|
-
} else if (sortBy === "description") {
|
|
191
|
-
aValue = getMetadataString(a.metadata, "description") || "";
|
|
192
|
-
bValue = getMetadataString(b.metadata, "description") || "";
|
|
193
|
-
} else {
|
|
194
|
-
aValue =
|
|
195
|
-
typeof a.metadata === "object" &&
|
|
196
|
-
a.metadata &&
|
|
197
|
-
"mtime" in a.metadata &&
|
|
198
|
-
typeof a.metadata.mtime === "number"
|
|
199
|
-
? a.metadata.mtime
|
|
200
|
-
: 0;
|
|
201
|
-
bValue =
|
|
202
|
-
typeof b.metadata === "object" &&
|
|
203
|
-
b.metadata &&
|
|
204
|
-
"mtime" in b.metadata &&
|
|
205
|
-
typeof b.metadata.mtime === "number"
|
|
206
|
-
? b.metadata.mtime
|
|
207
|
-
: 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (sortOrder === "desc") {
|
|
211
|
-
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
|
|
212
|
-
} else {
|
|
213
|
-
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Apply pagination
|
|
219
|
-
const totalCount = filteredFiles.length;
|
|
220
|
-
const totalPages = Math.ceil(totalCount / pageSize);
|
|
221
|
-
const hasNextPage = offset + pageSize < totalCount;
|
|
222
|
-
const hasPreviousPage = page > 1;
|
|
223
|
-
const items = filteredFiles.slice(offset, offset + pageSize);
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
items: items.map(({ resourceId, metadata }) => {
|
|
227
|
-
// Construct Resources 2.0 URI
|
|
228
|
-
const uri = ResourceUri.build(
|
|
229
|
-
env.DECO_REQUEST_CONTEXT.integrationId as string,
|
|
230
|
-
resourceName,
|
|
231
|
-
resourceId,
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// Extract title and description from metadata, with fallbacks
|
|
235
|
-
const name = getMetadataString(metadata, "name") || resourceId;
|
|
236
|
-
const description =
|
|
237
|
-
getMetadataString(metadata, "description") || "";
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
uri,
|
|
241
|
-
data: { name, description },
|
|
242
|
-
created_at:
|
|
243
|
-
typeof metadata === "object" &&
|
|
244
|
-
metadata &&
|
|
245
|
-
"ctime" in metadata &&
|
|
246
|
-
typeof metadata.ctime === "number"
|
|
247
|
-
? new Date(metadata.ctime).toISOString()
|
|
248
|
-
: undefined,
|
|
249
|
-
updated_at:
|
|
250
|
-
typeof metadata === "object" &&
|
|
251
|
-
metadata &&
|
|
252
|
-
"mtime" in metadata &&
|
|
253
|
-
typeof metadata.mtime === "number"
|
|
254
|
-
? new Date(metadata.mtime).toISOString()
|
|
255
|
-
: undefined,
|
|
256
|
-
created_by: getMetadataString(metadata, "createdBy"),
|
|
257
|
-
updated_by:
|
|
258
|
-
getMetadataString(metadata, "updatedBy") ||
|
|
259
|
-
getMetadataString(metadata, "createdBy"),
|
|
260
|
-
};
|
|
261
|
-
}),
|
|
262
|
-
totalCount,
|
|
263
|
-
page,
|
|
264
|
-
pageSize,
|
|
265
|
-
totalPages,
|
|
266
|
-
hasNextPage,
|
|
267
|
-
hasPreviousPage,
|
|
268
|
-
};
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
// deco_resource_read
|
|
273
|
-
{
|
|
274
|
-
description:
|
|
275
|
-
enhancements?.[
|
|
276
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_READ` as keyof typeof enhancements
|
|
277
|
-
]?.description ||
|
|
278
|
-
`Read a ${resourceName} resource from the DECONFIG directory ${directory}`,
|
|
279
|
-
handler: async ({ uri }) => {
|
|
280
|
-
// Validate URI format
|
|
281
|
-
ResourceUriSchema.parse(uri);
|
|
282
|
-
|
|
283
|
-
const resourceId = ResourceUri.unwind(uri).resourceId;
|
|
284
|
-
const filePath = ResourcePath.build(directory, resourceId);
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const fileData = (await deconfig.READ_FILE({
|
|
288
|
-
path: filePath,
|
|
289
|
-
format: "plainString",
|
|
290
|
-
})) as {
|
|
291
|
-
content: string;
|
|
292
|
-
ctime: number;
|
|
293
|
-
mtime: number;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const content = fileData.content;
|
|
297
|
-
|
|
298
|
-
// Parse the JSON content
|
|
299
|
-
let parsedData: Record<string, unknown> = {};
|
|
300
|
-
try {
|
|
301
|
-
parsedData = JSON.parse(content);
|
|
302
|
-
} catch {
|
|
303
|
-
throw new UserInputError("Invalid JSON content in resource file");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Validate against schema
|
|
307
|
-
const validatedData = dataSchema.parse(parsedData);
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
uri,
|
|
311
|
-
data: validatedData,
|
|
312
|
-
created_at:
|
|
313
|
-
typeof fileData.ctime === "number"
|
|
314
|
-
? new Date(fileData.ctime).toISOString()
|
|
315
|
-
: new Date().toISOString(),
|
|
316
|
-
updated_at:
|
|
317
|
-
typeof fileData.mtime === "number"
|
|
318
|
-
? new Date(fileData.mtime).toISOString()
|
|
319
|
-
: new Date().toISOString(),
|
|
320
|
-
created_by:
|
|
321
|
-
parsedData &&
|
|
322
|
-
"created_by" in parsedData &&
|
|
323
|
-
typeof parsedData.created_by === "string"
|
|
324
|
-
? parsedData.created_by
|
|
325
|
-
: undefined,
|
|
326
|
-
updated_by:
|
|
327
|
-
parsedData &&
|
|
328
|
-
"updated_by" in parsedData &&
|
|
329
|
-
typeof parsedData.updated_by === "string"
|
|
330
|
-
? parsedData.updated_by
|
|
331
|
-
: undefined,
|
|
332
|
-
};
|
|
333
|
-
} catch (error) {
|
|
334
|
-
if (error instanceof Error && error.message.includes("not found")) {
|
|
335
|
-
throw new NotFoundError(`Resource not found: ${uri}`);
|
|
336
|
-
}
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
|
|
342
|
-
// deco_resource_create (optional)
|
|
343
|
-
{
|
|
344
|
-
description:
|
|
345
|
-
enhancements?.[
|
|
346
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_CREATE` as keyof typeof enhancements
|
|
347
|
-
]?.description ||
|
|
348
|
-
`Create a new ${resourceName} resource in the DECONFIG directory ${directory}`,
|
|
349
|
-
handler: async ({ data }) => {
|
|
350
|
-
// Validate data against schema
|
|
351
|
-
const validatedData = dataSchema.parse(data);
|
|
352
|
-
|
|
353
|
-
// Run semantic validation if provided
|
|
354
|
-
if (semanticValidate) {
|
|
355
|
-
await semanticValidate(validatedData);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Extract resource ID from name or generate one
|
|
359
|
-
const resourceId =
|
|
360
|
-
(validatedData.name as string)?.replace(/[^a-zA-Z0-9-_]/g, "-") ||
|
|
361
|
-
crypto.randomUUID();
|
|
362
|
-
const uri = ResourceUri.build(
|
|
363
|
-
env.DECO_REQUEST_CONTEXT.integrationId as string,
|
|
364
|
-
resourceName,
|
|
365
|
-
resourceId,
|
|
366
|
-
);
|
|
367
|
-
const filePath = ResourcePath.build(directory, resourceId);
|
|
368
|
-
const user = env.DECO_REQUEST_CONTEXT.ensureAuthenticated();
|
|
369
|
-
// Prepare resource data with metadata
|
|
370
|
-
const resourceData = {
|
|
371
|
-
...validatedData,
|
|
372
|
-
id: resourceId,
|
|
373
|
-
created_at: new Date().toISOString(),
|
|
374
|
-
updated_at: new Date().toISOString(),
|
|
375
|
-
created_by: user?.id ? String(user.id) : undefined,
|
|
376
|
-
updated_by: user?.id ? String(user.id) : undefined,
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const fileContent = JSON.stringify(resourceData, null, 2);
|
|
380
|
-
const putResult = (await deconfig.PUT_FILE({
|
|
381
|
-
path: filePath,
|
|
382
|
-
content: fileContent,
|
|
383
|
-
metadata: {
|
|
384
|
-
resourceType: resourceName,
|
|
385
|
-
resourceId,
|
|
386
|
-
createdBy: user?.id,
|
|
387
|
-
name: validatedData.name || resourceId,
|
|
388
|
-
description: validatedData.description || "",
|
|
389
|
-
},
|
|
390
|
-
})) as { conflict?: boolean };
|
|
391
|
-
|
|
392
|
-
if (putResult.conflict) {
|
|
393
|
-
throw new UserInputError(
|
|
394
|
-
"Resource write conflicted. Please refresh and retry.",
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
uri,
|
|
400
|
-
data: validatedData,
|
|
401
|
-
created_at: resourceData.created_at,
|
|
402
|
-
updated_at: resourceData.updated_at,
|
|
403
|
-
created_by: user?.id ? String(user.id) : undefined,
|
|
404
|
-
updated_by: user?.id ? String(user.id) : undefined,
|
|
405
|
-
};
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
// deco_resource_update (optional)
|
|
410
|
-
{
|
|
411
|
-
description:
|
|
412
|
-
enhancements?.[
|
|
413
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_UPDATE` as keyof typeof enhancements
|
|
414
|
-
]?.description ||
|
|
415
|
-
`Update a ${resourceName} resource in the DECONFIG directory ${directory}`,
|
|
416
|
-
handler: async ({ uri, data }) => {
|
|
417
|
-
// Validate URI format
|
|
418
|
-
ResourceUriSchema.parse(uri);
|
|
419
|
-
|
|
420
|
-
const resourceId = ResourceUri.unwind(uri).resourceId;
|
|
421
|
-
const filePath = ResourcePath.build(directory, resourceId);
|
|
422
|
-
|
|
423
|
-
// Read existing file to get current data
|
|
424
|
-
let existingData: Record<string, unknown> = {};
|
|
425
|
-
try {
|
|
426
|
-
const fileData = (await deconfig.READ_FILE({
|
|
427
|
-
path: filePath,
|
|
428
|
-
format: "plainString",
|
|
429
|
-
})) as { content: string };
|
|
430
|
-
existingData = JSON.parse(fileData.content);
|
|
431
|
-
} catch {
|
|
432
|
-
throw new NotFoundError(`Resource not found: ${uri}`);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Validate new data against schema
|
|
436
|
-
const validatedData = dataSchema.parse(data);
|
|
437
|
-
|
|
438
|
-
// Run semantic validation if provided
|
|
439
|
-
if (semanticValidate) {
|
|
440
|
-
await semanticValidate(validatedData);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const user = env.DECO_REQUEST_CONTEXT.ensureAuthenticated();
|
|
444
|
-
|
|
445
|
-
const previousCreatedBy =
|
|
446
|
-
typeof existingData["created_by"] === "string"
|
|
447
|
-
? (existingData["created_by"] as string)
|
|
448
|
-
: undefined;
|
|
449
|
-
|
|
450
|
-
// Merge existing data with updates
|
|
451
|
-
const updatedData = {
|
|
452
|
-
...existingData,
|
|
453
|
-
...validatedData,
|
|
454
|
-
id: resourceId,
|
|
455
|
-
createdBy: previousCreatedBy,
|
|
456
|
-
updated_at: new Date().toISOString(),
|
|
457
|
-
updated_by: user?.id ? String(user.id) : undefined,
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const fileContent = JSON.stringify(updatedData, null, 2);
|
|
461
|
-
|
|
462
|
-
const putResult = (await deconfig.PUT_FILE({
|
|
463
|
-
path: filePath,
|
|
464
|
-
content: fileContent,
|
|
465
|
-
metadata: {
|
|
466
|
-
resourceType: resourceName,
|
|
467
|
-
resourceId,
|
|
468
|
-
updatedBy: user?.id,
|
|
469
|
-
name: validatedData.name || resourceId,
|
|
470
|
-
description: validatedData.description || "",
|
|
471
|
-
},
|
|
472
|
-
})) as { conflict?: boolean };
|
|
473
|
-
|
|
474
|
-
if (putResult.conflict) {
|
|
475
|
-
throw new UserInputError(
|
|
476
|
-
"Resource write conflicted. Please refresh and retry.",
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
uri,
|
|
482
|
-
data: validatedData,
|
|
483
|
-
created_at: existingData.created_at as string,
|
|
484
|
-
updated_at: updatedData.updated_at,
|
|
485
|
-
created_by: existingData.created_by as string,
|
|
486
|
-
updated_by: user?.id ? String(user.id) : undefined,
|
|
487
|
-
};
|
|
488
|
-
},
|
|
489
|
-
},
|
|
490
|
-
// deco_resource_delete (optional)
|
|
491
|
-
{
|
|
492
|
-
description:
|
|
493
|
-
enhancements?.[
|
|
494
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_DELETE` as keyof typeof enhancements
|
|
495
|
-
]?.description ||
|
|
496
|
-
`Delete a ${resourceName} resource from the DECONFIG directory ${directory}`,
|
|
497
|
-
handler: async ({ uri }) => {
|
|
498
|
-
// Validate URI format
|
|
499
|
-
ResourceUriSchema.parse(uri);
|
|
500
|
-
|
|
501
|
-
const resourceId = ResourceUri.unwind(uri).resourceId;
|
|
502
|
-
const filePath = ResourcePath.build(directory, resourceId);
|
|
503
|
-
|
|
504
|
-
try {
|
|
505
|
-
await deconfig.DELETE_FILE({
|
|
506
|
-
path: filePath,
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
success: true,
|
|
511
|
-
uri,
|
|
512
|
-
};
|
|
513
|
-
} catch (error) {
|
|
514
|
-
if (error instanceof Error && error.message.includes("not found")) {
|
|
515
|
-
throw new NotFoundError(`Resource not found: ${uri}`);
|
|
516
|
-
}
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
},
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
description:
|
|
523
|
-
enhancements?.[
|
|
524
|
-
`DECO_RESOURCE_${resourceName.toUpperCase()}_DESCRIBE` as keyof typeof enhancements
|
|
525
|
-
]?.description ||
|
|
526
|
-
`Describe the ${resourceName} resource in the DECONFIG directory ${directory}`,
|
|
527
|
-
handler: () => {
|
|
528
|
-
return {
|
|
529
|
-
uriTemplate: ResourceUri.build(
|
|
530
|
-
env.DECO_REQUEST_CONTEXT.integrationId as string,
|
|
531
|
-
resourceName,
|
|
532
|
-
"*",
|
|
533
|
-
),
|
|
534
|
-
features: {
|
|
535
|
-
watch: {
|
|
536
|
-
pathname: `${RESOURCE_WATCH_BASE_PATHNAME}${directory}`,
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
};
|
|
540
|
-
},
|
|
541
|
-
},
|
|
542
|
-
]);
|
|
543
|
-
|
|
544
|
-
return tools;
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
const removeLeadingSlash = (url: string) => {
|
|
548
|
-
return url.startsWith("/") ? url.slice(1) : url;
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
export interface WatchOptions {
|
|
552
|
-
watcherId?: string;
|
|
553
|
-
pathFilter: string;
|
|
554
|
-
resourceName: string;
|
|
555
|
-
env: DefaultEnv & { DECONFIG: DeconfigClient };
|
|
556
|
-
}
|
|
557
|
-
const watcher = ({
|
|
558
|
-
env,
|
|
559
|
-
pathFilter,
|
|
560
|
-
resourceName,
|
|
561
|
-
...options
|
|
562
|
-
}: WatchOptions): AsyncIterableIterator<{ uri: string }> => {
|
|
563
|
-
const url = new URL(
|
|
564
|
-
`/${removeLeadingSlash(env.DECO_REQUEST_CONTEXT.workspace)}/deconfig/watch`,
|
|
565
|
-
`${env.DECO_API_URL ?? "https://api.decocms.com"}`,
|
|
566
|
-
);
|
|
567
|
-
if (options.watcherId) {
|
|
568
|
-
url.searchParams.set("watcher-id", options.watcherId);
|
|
569
|
-
}
|
|
570
|
-
url.searchParams.set("path-filter", pathFilter);
|
|
571
|
-
url.searchParams.set("branch", env.DECO_REQUEST_CONTEXT.branch ?? "main");
|
|
572
|
-
url.searchParams.set("auth-token", env.DECO_REQUEST_CONTEXT.token);
|
|
573
|
-
url.searchParams.set("from-ctime", "1");
|
|
574
|
-
|
|
575
|
-
const eventSource = new EventSource(url);
|
|
576
|
-
const it = toAsyncIterator<{
|
|
577
|
-
path: string;
|
|
578
|
-
metadata: { address: string };
|
|
579
|
-
}>(eventSource, "change");
|
|
580
|
-
const iterator = async function* () {
|
|
581
|
-
for await (const event of it) {
|
|
582
|
-
const { path } = event;
|
|
583
|
-
try {
|
|
584
|
-
const { resourceId } = ResourcePath.extract(path);
|
|
585
|
-
const uri = ResourceUri.build(
|
|
586
|
-
env.DECO_REQUEST_CONTEXT.integrationId as string,
|
|
587
|
-
resourceName,
|
|
588
|
-
resourceId,
|
|
589
|
-
);
|
|
590
|
-
yield { uri };
|
|
591
|
-
} catch {
|
|
592
|
-
// ignore
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
const mIterator = iterator();
|
|
597
|
-
const retn = mIterator.return;
|
|
598
|
-
mIterator.return = function (val) {
|
|
599
|
-
eventSource.close();
|
|
600
|
-
return retn?.call(mIterator, val) ?? val;
|
|
601
|
-
};
|
|
602
|
-
return mIterator;
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const hasDeconfigBinding = (
|
|
606
|
-
env: unknown,
|
|
607
|
-
): env is DefaultEnv & { DECONFIG: DeconfigClient } => {
|
|
608
|
-
return (
|
|
609
|
-
env !== undefined &&
|
|
610
|
-
typeof env === "object" &&
|
|
611
|
-
env !== null &&
|
|
612
|
-
"DECONFIG" in env
|
|
613
|
-
);
|
|
614
|
-
};
|
|
615
|
-
export const RESOURCE_WATCH_BASE_PATHNAME = "/resources/watch";
|
|
616
|
-
export const DeconfigResource = {
|
|
617
|
-
WatchPathNameBase: RESOURCE_WATCH_BASE_PATHNAME,
|
|
618
|
-
watchAPI: (req: Request, env: DefaultEnv) => {
|
|
619
|
-
if (!hasDeconfigBinding(env)) {
|
|
620
|
-
return new Response("DECONFIG:@deco/deconfig binding is required", {
|
|
621
|
-
status: 400,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const url = new URL(req.url);
|
|
626
|
-
const uri = url.searchParams.get("uri");
|
|
627
|
-
if (!uri) {
|
|
628
|
-
return new Response("URI is required", { status: 400 });
|
|
629
|
-
}
|
|
630
|
-
const pathname = url.pathname;
|
|
631
|
-
let pathFilter = pathname.slice(RESOURCE_WATCH_BASE_PATHNAME.length); // removes `${RESOURCE_WATCH_BASE_PATHNAME}`
|
|
632
|
-
const { resourceName, resourceId } = ResourceUri.unwind(uri);
|
|
633
|
-
pathFilter =
|
|
634
|
-
resourceId === "*"
|
|
635
|
-
? pathFilter
|
|
636
|
-
: ResourcePath.build(pathFilter, resourceId);
|
|
637
|
-
const watch = watcher({
|
|
638
|
-
env,
|
|
639
|
-
resourceName,
|
|
640
|
-
pathFilter,
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// Create SSE-compatible ReadableStream
|
|
644
|
-
const sseStream = new ReadableStream({
|
|
645
|
-
async start(controller) {
|
|
646
|
-
const encoder = new TextEncoder();
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
for await (const event of watch) {
|
|
650
|
-
// Format as SSE: data: {json}\n\n
|
|
651
|
-
const sseData = `data: ${JSON.stringify(event)}\n\n`;
|
|
652
|
-
controller.enqueue(encoder.encode(sseData));
|
|
653
|
-
}
|
|
654
|
-
controller.close();
|
|
655
|
-
} catch (error) {
|
|
656
|
-
controller.error(error);
|
|
657
|
-
}
|
|
658
|
-
},
|
|
659
|
-
cancel() {
|
|
660
|
-
watch.return?.();
|
|
661
|
-
// Clean up the async iterator if needed
|
|
662
|
-
},
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
return new Response(sseStream, {
|
|
666
|
-
status: 200,
|
|
667
|
-
headers: {
|
|
668
|
-
"Content-Type": "text/event-stream",
|
|
669
|
-
"Cache-Control": "no-cache",
|
|
670
|
-
Connection: "keep-alive",
|
|
671
|
-
"Access-Control-Allow-Origin": WELL_KNOWN_ORIGINS.join(","),
|
|
672
|
-
"Access-Control-Allow-Headers": "Cache-Control",
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
},
|
|
676
|
-
define: <TDataSchema extends BaseResourceDataSchema>(
|
|
677
|
-
options: Omit<DeconfigResourceOptions<TDataSchema>, "env">,
|
|
678
|
-
) => {
|
|
679
|
-
return {
|
|
680
|
-
watcher,
|
|
681
|
-
create: (env: DefaultEnv & { DECONFIG: DeconfigClient }) => {
|
|
682
|
-
return createDeconfigResource({
|
|
683
|
-
env,
|
|
684
|
-
...options,
|
|
685
|
-
});
|
|
686
|
-
},
|
|
687
|
-
};
|
|
688
|
-
},
|
|
689
|
-
};
|