digital-products 2.1.3 → 2.4.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.
- package/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/dist/api.js +7 -7
- package/dist/api.js.map +1 -1
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/client.d.ts +157 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +69 -0
- package/dist/client.js.map +1 -0
- package/dist/content.js +7 -7
- package/dist/content.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +6 -6
- package/dist/data.js.map +1 -1
- package/dist/dataset.js +5 -5
- package/dist/dataset.js.map +1 -1
- package/dist/index.d.ts +92 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +17 -10
- package/dist/mcp.js.map +1 -1
- package/dist/product.js +2 -2
- package/dist/product.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +52 -16
- package/dist/sdk.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +12 -8
- package/dist/site.js.map +1 -1
- package/dist/types.d.ts +830 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +495 -2
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +205 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +356 -0
- package/dist/worker.js.map +1 -0
- package/package.json +29 -13
- package/src/api.ts +7 -7
- package/src/app.ts +6 -6
- package/src/client.ts +192 -0
- package/src/content.ts +7 -7
- package/src/data.ts +12 -7
- package/src/dataset.ts +5 -5
- package/src/index.ts +151 -15
- package/src/mcp.ts +18 -11
- package/src/product.ts +2 -2
- package/src/sdk.ts +54 -15
- package/src/site.ts +12 -8
- package/src/types.ts +821 -12
- package/src/worker.ts +525 -0
- package/test/product.test.ts +53 -198
- package/test/unified-types.test.ts +589 -0
- package/test/worker.test.ts +912 -0
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/LICENSE +0 -21
- package/dist/features/define.d.ts +0 -63
- package/dist/features/define.d.ts.map +0 -1
- package/dist/features/define.js +0 -72
- package/dist/features/define.js.map +0 -1
- package/dist/features/flags.d.ts +0 -98
- package/dist/features/flags.d.ts.map +0 -1
- package/dist/features/flags.js +0 -145
- package/dist/features/flags.js.map +0 -1
- package/dist/features/toggles.d.ts +0 -75
- package/dist/features/toggles.d.ts.map +0 -1
- package/dist/features/toggles.js +0 -107
- package/dist/features/toggles.js.map +0 -1
- package/dist/tiers/define.d.ts +0 -63
- package/dist/tiers/define.d.ts.map +0 -1
- package/dist/tiers/define.js +0 -78
- package/dist/tiers/define.js.map +0 -1
- package/dist/tiers/entitlements.d.ts +0 -94
- package/dist/tiers/entitlements.d.ts.map +0 -1
- package/dist/tiers/entitlements.js +0 -94
- package/dist/tiers/entitlements.js.map +0 -1
- package/src/api.js +0 -128
- package/src/app.js +0 -106
- package/src/content.js +0 -77
- package/src/data.js +0 -106
- package/src/dataset.js +0 -49
- package/src/entities/ai.js +0 -858
- package/src/entities/content.js +0 -783
- package/src/entities/index.js +0 -88
- package/src/entities/interfaces.js +0 -929
- package/src/entities/lifecycle.js +0 -803
- package/src/entities/products.js +0 -797
- package/src/entities/web.js +0 -657
- package/src/features/define.ts +0 -130
- package/src/features/flags.ts +0 -247
- package/src/features/toggles.ts +0 -189
- package/src/index.js +0 -35
- package/src/mcp.js +0 -139
- package/src/pricing/billing.ts +0 -386
- package/src/pricing/plans.ts +0 -214
- package/src/product.js +0 -53
- package/src/registry.js +0 -31
- package/src/sdk.js +0 -127
- package/src/site.js +0 -112
- package/src/tiers/define.ts +0 -137
- package/src/tiers/entitlements.ts +0 -201
- package/src/types.js +0 -4
- package/test/analytics/events.test.ts +0 -319
- package/test/analytics/experiments.test.ts +0 -327
- package/test/features/define.test.ts +0 -187
- package/test/features/flags.test.ts +0 -259
- package/test/features/toggles.test.ts +0 -178
- package/test/lifecycle/stages.test.ts +0 -233
- package/test/lifecycle/transitions.test.ts +0 -207
- package/test/onboarding/flows.test.ts +0 -307
- package/test/pricing/billing.test.ts +0 -287
- package/test/pricing/plans.test.ts +0 -307
- package/test/roadmap/milestones.test.ts +0 -231
- package/test/roadmap/priorities.test.ts +0 -239
- package/test/tiers/define.test.ts +0 -192
- package/test/tiers/entitlements.test.ts +0 -220
package/src/worker.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Export - WorkerEntrypoint for RPC access to Digital Products
|
|
3
|
+
*
|
|
4
|
+
* Exposes ProductServiceCore methods via Cloudflare RPC.
|
|
5
|
+
* Uses in-memory storage for products, features, versions, and bundles.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // wrangler.jsonc
|
|
10
|
+
* {
|
|
11
|
+
* "services": [
|
|
12
|
+
* { "binding": "PRODUCTS", "service": "digital-products" }
|
|
13
|
+
* ]
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* // worker.ts - consuming service
|
|
17
|
+
* export default {
|
|
18
|
+
* async fetch(request: Request, env: Env) {
|
|
19
|
+
* const service = env.PRODUCTS.connect()
|
|
20
|
+
* const product = await service.create({ name: 'My Product', description: 'Test', version: '1.0.0' })
|
|
21
|
+
* return Response.json(product)
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// @ts-expect-error cloudflare:workers is a Cloudflare Workers runtime module
|
|
30
|
+
import { WorkerEntrypoint, RpcTarget } from 'cloudflare:workers'
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Types
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
export interface ProductData {
|
|
37
|
+
id: string
|
|
38
|
+
name: string
|
|
39
|
+
description: string
|
|
40
|
+
version: string
|
|
41
|
+
status: 'draft' | 'active' | 'deprecated' | 'archived'
|
|
42
|
+
type?: string
|
|
43
|
+
features?: FeatureData[]
|
|
44
|
+
metadata?: Record<string, unknown>
|
|
45
|
+
tags?: string[]
|
|
46
|
+
createdAt: Date
|
|
47
|
+
updatedAt: Date
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FeatureData {
|
|
51
|
+
id: string
|
|
52
|
+
name: string
|
|
53
|
+
description: string
|
|
54
|
+
status: 'draft' | 'beta' | 'ga' | 'deprecated'
|
|
55
|
+
productId: string
|
|
56
|
+
metadata?: Record<string, unknown>
|
|
57
|
+
createdAt: Date
|
|
58
|
+
updatedAt: Date
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface VersionData {
|
|
62
|
+
id: string
|
|
63
|
+
productId: string
|
|
64
|
+
version: string
|
|
65
|
+
changelog?: string
|
|
66
|
+
status: 'draft' | 'published' | 'deprecated'
|
|
67
|
+
features?: string[]
|
|
68
|
+
metadata?: Record<string, unknown>
|
|
69
|
+
createdAt: Date
|
|
70
|
+
publishedAt?: Date
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BundleData {
|
|
74
|
+
id: string
|
|
75
|
+
name: string
|
|
76
|
+
description: string
|
|
77
|
+
productIds: string[]
|
|
78
|
+
pricing?: PricingData
|
|
79
|
+
metadata?: Record<string, unknown>
|
|
80
|
+
createdAt: Date
|
|
81
|
+
updatedAt: Date
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PricingData {
|
|
85
|
+
model: 'free' | 'one-time' | 'subscription' | 'usage-based'
|
|
86
|
+
amount?: number
|
|
87
|
+
currency?: string
|
|
88
|
+
interval?: 'month' | 'year'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ListOptions {
|
|
92
|
+
limit?: number
|
|
93
|
+
offset?: number
|
|
94
|
+
status?: string
|
|
95
|
+
type?: string
|
|
96
|
+
orderBy?: string
|
|
97
|
+
order?: 'asc' | 'desc'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* DurableObjectNamespace type declaration for Cloudflare Workers
|
|
102
|
+
*/
|
|
103
|
+
declare interface DurableObjectNamespace {
|
|
104
|
+
idFromName(name: string): DurableObjectId
|
|
105
|
+
idFromString(id: string): DurableObjectId
|
|
106
|
+
newUniqueId(): DurableObjectId
|
|
107
|
+
get(id: DurableObjectId): DurableObjectStub
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare interface DurableObjectId {
|
|
111
|
+
toString(): string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
declare interface DurableObjectStub {
|
|
115
|
+
fetch(request: Request): Promise<Response>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Environment bindings for the worker
|
|
120
|
+
*/
|
|
121
|
+
export interface Env {
|
|
122
|
+
AI?: unknown
|
|
123
|
+
PRODUCT_CATALOG?: DurableObjectNamespace
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// In-Memory Storage
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
// Global storage maps for persistence across connect() calls
|
|
131
|
+
const products = new Map<string, ProductData>()
|
|
132
|
+
const features = new Map<string, FeatureData>()
|
|
133
|
+
const versions = new Map<string, VersionData>()
|
|
134
|
+
const bundles = new Map<string, BundleData>()
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate a unique ID
|
|
138
|
+
*/
|
|
139
|
+
function generateId(): string {
|
|
140
|
+
return crypto.randomUUID()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// ProductServiceCore
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* ProductServiceCore - RpcTarget with all product operations
|
|
149
|
+
*
|
|
150
|
+
* Provides CRUD for products, features, versions, and bundles.
|
|
151
|
+
*/
|
|
152
|
+
export class ProductServiceCore extends RpcTarget {
|
|
153
|
+
private env: Env
|
|
154
|
+
|
|
155
|
+
constructor(env: Env) {
|
|
156
|
+
super()
|
|
157
|
+
this.env = env
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ==================== Product CRUD ====================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a new product
|
|
164
|
+
*/
|
|
165
|
+
async create(data: Partial<ProductData>): Promise<ProductData> {
|
|
166
|
+
// Validate required fields
|
|
167
|
+
if (!data.name || !data.description || !data.version) {
|
|
168
|
+
throw new Error('Missing required fields: name, description, and version are required')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const now = new Date()
|
|
172
|
+
const product: ProductData = {
|
|
173
|
+
id: data.id || generateId(),
|
|
174
|
+
name: data.name,
|
|
175
|
+
description: data.description,
|
|
176
|
+
version: data.version,
|
|
177
|
+
status: data.status || 'draft',
|
|
178
|
+
createdAt: now,
|
|
179
|
+
updatedAt: now,
|
|
180
|
+
...(data.type !== undefined && { type: data.type }),
|
|
181
|
+
...(data.metadata !== undefined && { metadata: data.metadata }),
|
|
182
|
+
...(data.tags !== undefined && { tags: data.tags }),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
products.set(product.id, product)
|
|
186
|
+
return product
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get a product by ID
|
|
191
|
+
*/
|
|
192
|
+
async get(id: string): Promise<ProductData | null> {
|
|
193
|
+
return products.get(id) || null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Update a product
|
|
198
|
+
*/
|
|
199
|
+
async update(id: string, data: Partial<ProductData>): Promise<ProductData> {
|
|
200
|
+
const existing = products.get(id)
|
|
201
|
+
if (!existing) {
|
|
202
|
+
throw new Error(`Product not found: ${id}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const updated: ProductData = {
|
|
206
|
+
...existing,
|
|
207
|
+
...(data as unknown as Record<string, unknown>),
|
|
208
|
+
id: existing.id, // ID cannot be changed
|
|
209
|
+
createdAt: existing.createdAt, // createdAt cannot be changed
|
|
210
|
+
updatedAt: new Date(),
|
|
211
|
+
// Merge metadata if provided
|
|
212
|
+
...(data.metadata ? { metadata: { ...existing.metadata, ...data.metadata } } : {}),
|
|
213
|
+
} as unknown as ProductData
|
|
214
|
+
|
|
215
|
+
products.set(id, updated)
|
|
216
|
+
return updated
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Delete a product
|
|
221
|
+
*/
|
|
222
|
+
async delete(id: string): Promise<boolean> {
|
|
223
|
+
const exists = products.has(id)
|
|
224
|
+
if (!exists) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Cascade delete features
|
|
229
|
+
for (const [featureId, feature] of features) {
|
|
230
|
+
if (feature.productId === id) {
|
|
231
|
+
features.delete(featureId)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Cascade delete versions
|
|
236
|
+
for (const [versionId, version] of versions) {
|
|
237
|
+
if (version.productId === id) {
|
|
238
|
+
versions.delete(versionId)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
products.delete(id)
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* List products with optional filtering
|
|
248
|
+
*/
|
|
249
|
+
async list(options?: ListOptions): Promise<ProductData[]> {
|
|
250
|
+
let result = Array.from(products.values())
|
|
251
|
+
|
|
252
|
+
// Filter by status
|
|
253
|
+
if (options?.status) {
|
|
254
|
+
result = result.filter((p) => p.status === options.status)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Filter by type
|
|
258
|
+
if (options?.type) {
|
|
259
|
+
result = result.filter((p) => p.type === options.type)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Sort
|
|
263
|
+
if (options?.orderBy) {
|
|
264
|
+
const order = options.order || 'asc'
|
|
265
|
+
result.sort((a, b) => {
|
|
266
|
+
const aVal = (a as unknown as Record<string, unknown>)[options.orderBy!]
|
|
267
|
+
const bVal = (b as unknown as Record<string, unknown>)[options.orderBy!]
|
|
268
|
+
|
|
269
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
270
|
+
return order === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (aVal instanceof Date && bVal instanceof Date) {
|
|
274
|
+
return order === 'asc' ? aVal.getTime() - bVal.getTime() : bVal.getTime() - aVal.getTime()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return 0
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Pagination
|
|
282
|
+
const offset = options?.offset || 0
|
|
283
|
+
const limit = options?.limit || result.length
|
|
284
|
+
result = result.slice(offset, offset + limit)
|
|
285
|
+
|
|
286
|
+
return result
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ==================== Feature Operations ====================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Add a feature to a product
|
|
293
|
+
*/
|
|
294
|
+
async addFeature(productId: string, featureData: Partial<FeatureData>): Promise<FeatureData> {
|
|
295
|
+
const product = products.get(productId)
|
|
296
|
+
if (!product) {
|
|
297
|
+
throw new Error(`Product not found: ${productId}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!featureData.name || !featureData.description) {
|
|
301
|
+
throw new Error('Missing required fields: name and description are required')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const now = new Date()
|
|
305
|
+
const feature: FeatureData = {
|
|
306
|
+
id: featureData.id || generateId(),
|
|
307
|
+
name: featureData.name,
|
|
308
|
+
description: featureData.description,
|
|
309
|
+
status: featureData.status || 'draft',
|
|
310
|
+
productId,
|
|
311
|
+
createdAt: now,
|
|
312
|
+
updatedAt: now,
|
|
313
|
+
...(featureData.metadata !== undefined && { metadata: featureData.metadata }),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
features.set(feature.id, feature)
|
|
317
|
+
return feature
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Update a feature
|
|
322
|
+
*/
|
|
323
|
+
async updateFeature(featureId: string, data: Partial<FeatureData>): Promise<FeatureData> {
|
|
324
|
+
const existing = features.get(featureId)
|
|
325
|
+
if (!existing) {
|
|
326
|
+
throw new Error(`Feature not found: ${featureId}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const updated: FeatureData = {
|
|
330
|
+
...existing,
|
|
331
|
+
...data,
|
|
332
|
+
id: existing.id,
|
|
333
|
+
productId: existing.productId,
|
|
334
|
+
createdAt: existing.createdAt,
|
|
335
|
+
updatedAt: new Date(),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
features.set(featureId, updated)
|
|
339
|
+
return updated
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Remove a feature
|
|
344
|
+
*/
|
|
345
|
+
async removeFeature(featureId: string): Promise<boolean> {
|
|
346
|
+
return features.delete(featureId)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* List features for a product
|
|
351
|
+
*/
|
|
352
|
+
async listFeatures(productId: string): Promise<FeatureData[]> {
|
|
353
|
+
return Array.from(features.values()).filter((f) => f.productId === productId)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ==================== Version Operations ====================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Publish a new version
|
|
360
|
+
*/
|
|
361
|
+
async publish(productId: string, versionStr: string, changelog?: string): Promise<VersionData> {
|
|
362
|
+
const product = products.get(productId)
|
|
363
|
+
if (!product) {
|
|
364
|
+
throw new Error(`Product not found: ${productId}`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Check for duplicate version
|
|
368
|
+
const existingVersion = Array.from(versions.values()).find(
|
|
369
|
+
(v) => v.productId === productId && v.version === versionStr
|
|
370
|
+
)
|
|
371
|
+
if (existingVersion) {
|
|
372
|
+
throw new Error(`Version ${versionStr} already exists for product ${productId}`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const now = new Date()
|
|
376
|
+
const version: VersionData = {
|
|
377
|
+
id: generateId(),
|
|
378
|
+
productId,
|
|
379
|
+
version: versionStr,
|
|
380
|
+
status: 'published',
|
|
381
|
+
createdAt: now,
|
|
382
|
+
publishedAt: now,
|
|
383
|
+
...(changelog !== undefined && { changelog }),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
versions.set(version.id, version)
|
|
387
|
+
return version
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* List versions for a product
|
|
392
|
+
*/
|
|
393
|
+
async listVersions(productId: string): Promise<VersionData[]> {
|
|
394
|
+
return Array.from(versions.values()).filter((v) => v.productId === productId)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get a specific version
|
|
399
|
+
*/
|
|
400
|
+
async getVersion(productId: string, versionStr: string): Promise<VersionData | null> {
|
|
401
|
+
return (
|
|
402
|
+
Array.from(versions.values()).find(
|
|
403
|
+
(v) => v.productId === productId && v.version === versionStr
|
|
404
|
+
) || null
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Deprecate a version
|
|
410
|
+
*/
|
|
411
|
+
async deprecateVersion(productId: string, versionStr: string): Promise<VersionData> {
|
|
412
|
+
const version = Array.from(versions.values()).find(
|
|
413
|
+
(v) => v.productId === productId && v.version === versionStr
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if (!version) {
|
|
417
|
+
throw new Error(`Version ${versionStr} not found for product ${productId}`)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
version.status = 'deprecated'
|
|
421
|
+
versions.set(version.id, version)
|
|
422
|
+
return version
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ==================== Bundle Operations ====================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Compose a bundle from multiple products
|
|
429
|
+
*/
|
|
430
|
+
async compose(
|
|
431
|
+
name: string,
|
|
432
|
+
productIds: string[],
|
|
433
|
+
options?: Partial<BundleData>
|
|
434
|
+
): Promise<BundleData> {
|
|
435
|
+
if (!productIds || productIds.length === 0) {
|
|
436
|
+
throw new Error('Bundle must contain at least one product')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Verify all products exist
|
|
440
|
+
for (const productId of productIds) {
|
|
441
|
+
if (!products.has(productId)) {
|
|
442
|
+
throw new Error(`Product not found: ${productId}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const now = new Date()
|
|
447
|
+
const bundle: BundleData = {
|
|
448
|
+
id: generateId(),
|
|
449
|
+
name,
|
|
450
|
+
description: options?.description || '',
|
|
451
|
+
productIds,
|
|
452
|
+
createdAt: now,
|
|
453
|
+
updatedAt: now,
|
|
454
|
+
...(options?.pricing !== undefined && { pricing: options.pricing }),
|
|
455
|
+
...(options?.metadata !== undefined && { metadata: options.metadata }),
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
bundles.set(bundle.id, bundle)
|
|
459
|
+
return bundle
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get a bundle by ID
|
|
464
|
+
*/
|
|
465
|
+
async getBundle(bundleId: string): Promise<BundleData | null> {
|
|
466
|
+
return bundles.get(bundleId) || null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Update a bundle
|
|
471
|
+
*/
|
|
472
|
+
async updateBundle(bundleId: string, data: Partial<BundleData>): Promise<BundleData> {
|
|
473
|
+
const existing = bundles.get(bundleId)
|
|
474
|
+
if (!existing) {
|
|
475
|
+
throw new Error(`Bundle not found: ${bundleId}`)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const updated: BundleData = {
|
|
479
|
+
...existing,
|
|
480
|
+
...data,
|
|
481
|
+
id: existing.id,
|
|
482
|
+
createdAt: existing.createdAt,
|
|
483
|
+
updatedAt: new Date(),
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
bundles.set(bundleId, updated)
|
|
487
|
+
return updated
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* List all bundles
|
|
492
|
+
*/
|
|
493
|
+
async listBundles(): Promise<BundleData[]> {
|
|
494
|
+
return Array.from(bundles.values())
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// =============================================================================
|
|
499
|
+
// ProductService (WorkerEntrypoint)
|
|
500
|
+
// =============================================================================
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* ProductService - WorkerEntrypoint for RPC access
|
|
504
|
+
*
|
|
505
|
+
* Provides `connect()` method that returns a ProductServiceCore instance
|
|
506
|
+
* with all product management operations.
|
|
507
|
+
*/
|
|
508
|
+
export class ProductService extends WorkerEntrypoint<Env> {
|
|
509
|
+
// Declare env property from WorkerEntrypoint
|
|
510
|
+
declare env: Env
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Connect and get an RPC-enabled service
|
|
514
|
+
*
|
|
515
|
+
* @returns ProductServiceCore instance for RPC calls
|
|
516
|
+
*/
|
|
517
|
+
connect(): ProductServiceCore {
|
|
518
|
+
return new ProductServiceCore(this.env)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Default export for Cloudflare Workers
|
|
524
|
+
*/
|
|
525
|
+
export default ProductService
|