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