@mevdragon/vidfarm-devcli 0.2.13 → 0.3.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.
Files changed (200) hide show
  1. package/README.md +11 -4
  2. package/SKILL.director.md +97 -0
  3. package/SKILL.platform.md +66 -6
  4. package/dist/src/cli.js +137 -1524
  5. package/dist/src/dev-serve.js +169 -0
  6. package/package.json +5 -24
  7. package/.env.example +0 -60
  8. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +0 -275
  9. package/auto-create-hyperframe-templates/extractor-system-prompt.md +0 -128
  10. package/auto-create-hyperframe-templates/input-manifest.schema.json +0 -56
  11. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +0 -118
  12. package/auto-create-hyperframe-templates/production-graph.schema.json +0 -314
  13. package/auto-create-hyperframe-templates/runbook.md +0 -198
  14. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +0 -368
  15. package/auto-create-hyperframe-templates/template-plan.schema.json +0 -311
  16. package/auto-create-hyperframe-templates/template-planner-prompt.md +0 -144
  17. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +0 -599
  18. package/auto-create-templates/extractor-system-prompt.md +0 -119
  19. package/auto-create-templates/production-graph.schema.json +0 -276
  20. package/auto-create-templates/runbook.md +0 -140
  21. package/auto-create-templates/template-plan.schema.json +0 -301
  22. package/auto-create-templates/template-planner-prompt.md +0 -137
  23. package/dist/src/account-pages-legacy.js +0 -9336
  24. package/dist/src/account-pages-legacy.js.map +0 -1
  25. package/dist/src/account-pages.js +0 -61
  26. package/dist/src/account-pages.js.map +0 -1
  27. package/dist/src/app.js +0 -14023
  28. package/dist/src/app.js.map +0 -1
  29. package/dist/src/cli.js.map +0 -1
  30. package/dist/src/composition-runtime.js +0 -581
  31. package/dist/src/composition-runtime.js.map +0 -1
  32. package/dist/src/config.js +0 -140
  33. package/dist/src/config.js.map +0 -1
  34. package/dist/src/context.js +0 -492
  35. package/dist/src/context.js.map +0 -1
  36. package/dist/src/dev-app-legacy.js +0 -694
  37. package/dist/src/dev-app-legacy.js.map +0 -1
  38. package/dist/src/dev-app.js +0 -6
  39. package/dist/src/dev-app.js.map +0 -1
  40. package/dist/src/domain.js +0 -2
  41. package/dist/src/domain.js.map +0 -1
  42. package/dist/src/editor-chat-history.js +0 -82
  43. package/dist/src/editor-chat-history.js.map +0 -1
  44. package/dist/src/editor-chat.js +0 -311
  45. package/dist/src/editor-chat.js.map +0 -1
  46. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  47. package/dist/src/frontend/flockposter-cache-store.js.map +0 -1
  48. package/dist/src/frontend/homepage-client.js +0 -117
  49. package/dist/src/frontend/homepage-client.js.map +0 -1
  50. package/dist/src/frontend/homepage-shared.js +0 -4
  51. package/dist/src/frontend/homepage-shared.js.map +0 -1
  52. package/dist/src/frontend/homepage-store.js +0 -28
  53. package/dist/src/frontend/homepage-store.js.map +0 -1
  54. package/dist/src/frontend/homepage-view.js +0 -260
  55. package/dist/src/frontend/homepage-view.js.map +0 -1
  56. package/dist/src/frontend/page-runtime-client.js +0 -103
  57. package/dist/src/frontend/page-runtime-client.js.map +0 -1
  58. package/dist/src/frontend/page-runtime-store.js +0 -9
  59. package/dist/src/frontend/page-runtime-store.js.map +0 -1
  60. package/dist/src/frontend/sentry.js +0 -42
  61. package/dist/src/frontend/sentry.js.map +0 -1
  62. package/dist/src/frontend/template-editor-chat.js +0 -3767
  63. package/dist/src/frontend/template-editor-chat.js.map +0 -1
  64. package/dist/src/help-page.js +0 -333
  65. package/dist/src/help-page.js.map +0 -1
  66. package/dist/src/homepage.js +0 -919
  67. package/dist/src/homepage.js.map +0 -1
  68. package/dist/src/hyperframes/composition.js +0 -180
  69. package/dist/src/hyperframes/composition.js.map +0 -1
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/index.js.map +0 -1
  72. package/dist/src/instrument.js +0 -30
  73. package/dist/src/instrument.js.map +0 -1
  74. package/dist/src/lib/crypto.js +0 -45
  75. package/dist/src/lib/crypto.js.map +0 -1
  76. package/dist/src/lib/dev-log.js +0 -54
  77. package/dist/src/lib/dev-log.js.map +0 -1
  78. package/dist/src/lib/display-name.js +0 -11
  79. package/dist/src/lib/display-name.js.map +0 -1
  80. package/dist/src/lib/ids.js +0 -24
  81. package/dist/src/lib/ids.js.map +0 -1
  82. package/dist/src/lib/images.js +0 -19
  83. package/dist/src/lib/images.js.map +0 -1
  84. package/dist/src/lib/json.js +0 -15
  85. package/dist/src/lib/json.js.map +0 -1
  86. package/dist/src/lib/template-dna.js +0 -543
  87. package/dist/src/lib/template-dna.js.map +0 -1
  88. package/dist/src/lib/template-paths.js +0 -28
  89. package/dist/src/lib/template-paths.js.map +0 -1
  90. package/dist/src/lib/template-style-options.js +0 -203
  91. package/dist/src/lib/template-style-options.js.map +0 -1
  92. package/dist/src/lib/time.js +0 -7
  93. package/dist/src/lib/time.js.map +0 -1
  94. package/dist/src/lib/video-quality-harness.js +0 -60
  95. package/dist/src/lib/video-quality-harness.js.map +0 -1
  96. package/dist/src/page-runtime.js +0 -2
  97. package/dist/src/page-runtime.js.map +0 -1
  98. package/dist/src/page-shell.js +0 -2393
  99. package/dist/src/page-shell.js.map +0 -1
  100. package/dist/src/primitive-context.js +0 -299
  101. package/dist/src/primitive-context.js.map +0 -1
  102. package/dist/src/primitive-registry.js +0 -2181
  103. package/dist/src/primitive-registry.js.map +0 -1
  104. package/dist/src/primitive-sdk.js +0 -4
  105. package/dist/src/primitive-sdk.js.map +0 -1
  106. package/dist/src/primitives/remotion/html-image.js +0 -30
  107. package/dist/src/primitives/remotion/html-image.js.map +0 -1
  108. package/dist/src/primitives/remotion/media-slideshow.js +0 -60
  109. package/dist/src/primitives/remotion/media-slideshow.js.map +0 -1
  110. package/dist/src/react-page-shell.js +0 -35
  111. package/dist/src/react-page-shell.js.map +0 -1
  112. package/dist/src/ready-post-schedule-component.js +0 -1515
  113. package/dist/src/ready-post-schedule-component.js.map +0 -1
  114. package/dist/src/registry.js +0 -296
  115. package/dist/src/registry.js.map +0 -1
  116. package/dist/src/runtime.js +0 -31
  117. package/dist/src/runtime.js.map +0 -1
  118. package/dist/src/services/api-call-history.js +0 -249
  119. package/dist/src/services/api-call-history.js.map +0 -1
  120. package/dist/src/services/auth.js +0 -152
  121. package/dist/src/services/auth.js.map +0 -1
  122. package/dist/src/services/billing.js +0 -231
  123. package/dist/src/services/billing.js.map +0 -1
  124. package/dist/src/services/chat-threads.js +0 -92
  125. package/dist/src/services/chat-threads.js.map +0 -1
  126. package/dist/src/services/fork-access.js +0 -94
  127. package/dist/src/services/fork-access.js.map +0 -1
  128. package/dist/src/services/ghostcut.js +0 -132
  129. package/dist/src/services/ghostcut.js.map +0 -1
  130. package/dist/src/services/hyperframes.js +0 -1014
  131. package/dist/src/services/hyperframes.js.map +0 -1
  132. package/dist/src/services/job-capacity.js +0 -14
  133. package/dist/src/services/job-capacity.js.map +0 -1
  134. package/dist/src/services/job-logs.js +0 -197
  135. package/dist/src/services/job-logs.js.map +0 -1
  136. package/dist/src/services/jobs.js +0 -136
  137. package/dist/src/services/jobs.js.map +0 -1
  138. package/dist/src/services/media-processing.js +0 -743
  139. package/dist/src/services/media-processing.js.map +0 -1
  140. package/dist/src/services/primitive-media-lambda.js +0 -280
  141. package/dist/src/services/primitive-media-lambda.js.map +0 -1
  142. package/dist/src/services/providers.js +0 -2870
  143. package/dist/src/services/providers.js.map +0 -1
  144. package/dist/src/services/rate-limits.js +0 -237
  145. package/dist/src/services/rate-limits.js.map +0 -1
  146. package/dist/src/services/remotion.js +0 -688
  147. package/dist/src/services/remotion.js.map +0 -1
  148. package/dist/src/services/serverless-auth.js +0 -374
  149. package/dist/src/services/serverless-auth.js.map +0 -1
  150. package/dist/src/services/serverless-jobs.js +0 -1074
  151. package/dist/src/services/serverless-jobs.js.map +0 -1
  152. package/dist/src/services/serverless-provider-keys.js +0 -401
  153. package/dist/src/services/serverless-provider-keys.js.map +0 -1
  154. package/dist/src/services/serverless-records.js +0 -1086
  155. package/dist/src/services/serverless-records.js.map +0 -1
  156. package/dist/src/services/serverless-template-configs.js +0 -67
  157. package/dist/src/services/serverless-template-configs.js.map +0 -1
  158. package/dist/src/services/storage.js +0 -382
  159. package/dist/src/services/storage.js.map +0 -1
  160. package/dist/src/services/template-certification.js +0 -475
  161. package/dist/src/services/template-certification.js.map +0 -1
  162. package/dist/src/services/template-loader.js +0 -99
  163. package/dist/src/services/template-loader.js.map +0 -1
  164. package/dist/src/services/template-runtime-bundles.js +0 -217
  165. package/dist/src/services/template-runtime-bundles.js.map +0 -1
  166. package/dist/src/services/template-sources.js +0 -1017
  167. package/dist/src/services/template-sources.js.map +0 -1
  168. package/dist/src/services/video-normalization.js +0 -2
  169. package/dist/src/services/video-normalization.js.map +0 -1
  170. package/dist/src/services/webhooks.js +0 -62
  171. package/dist/src/services/webhooks.js.map +0 -1
  172. package/dist/src/template-editor-pages.js +0 -6624
  173. package/dist/src/template-editor-pages.js.map +0 -1
  174. package/dist/src/template-editor-shell.js +0 -2713
  175. package/dist/src/template-editor-shell.js.map +0 -1
  176. package/dist/src/template-sdk.js +0 -4
  177. package/dist/src/template-sdk.js.map +0 -1
  178. package/dist/src/worker.js +0 -17
  179. package/dist/src/worker.js.map +0 -1
  180. package/public/assets/homepage-app.js +0 -54
  181. package/public/assets/homepage-client-app.js +0 -80
  182. package/public/assets/page-runtime-client-app.js +0 -94
  183. package/readme.secret.md +0 -89
  184. package/templates/vidfarm_template_0000/README.md +0 -106
  185. package/templates/vidfarm_template_0000/SKILL.md +0 -266
  186. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  187. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  188. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  189. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  190. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  192. package/templates/vidfarm_template_0000/composition.json +0 -11
  193. package/templates/vidfarm_template_0000/src/lib/images.js +0 -221
  194. package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
  195. package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  196. package/templates/vidfarm_template_0000/src/sdk.js +0 -3
  197. package/templates/vidfarm_template_0000/src/style-options.js +0 -200
  198. package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  199. package/templates/vidfarm_template_0000/src/template.js +0 -1566
  200. package/templates/vidfarm_template_0000/template.config.json +0 -21
@@ -1,1086 +0,0 @@
1
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
- import { DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";
3
- import { config } from "../config.js";
4
- import { devLog } from "../lib/dev-log.js";
5
- import { displayNameFromEmail } from "../lib/display-name.js";
6
- import { createId, createIdV7 } from "../lib/ids.js";
7
- import { nowIso } from "../lib/time.js";
8
- import { ServerlessAuthService } from "./serverless-auth.js";
9
- const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
10
- marshallOptions: {
11
- removeUndefinedValues: true
12
- }
13
- });
14
- const METHOD_COLLECTIONS = {
15
- archiveReadyPost: "ready_post",
16
- createCompositionFork: "composition_fork",
17
- deleteCompositionFork: "composition_fork",
18
- getCompositionFork: "composition_fork",
19
- listCompositionForksForCustomer: "composition_fork",
20
- updateCompositionFork: "composition_fork",
21
- createEmailChannel: "email_channel",
22
- createPostSchedule: "post_schedule",
23
- createReadyPost: "ready_post",
24
- createTemplateRelease: "template_release",
25
- createTemplateSource: "template_source",
26
- createUserAttachment: "user_attachment",
27
- createUserFileFolder: "user_file_folder",
28
- createUserTemporaryFile: "user_temporary_file",
29
- deleteEmailChannel: "email_channel",
30
- deleteReadyPost: "ready_post",
31
- deleteUserAttachment: "user_attachment",
32
- deleteUserFileFolder: "user_file_folder",
33
- deleteUserTemporaryFile: "user_temporary_file",
34
- getEmailChannel: "email_channel",
35
- getEmailChannelByEmail: "email_channel",
36
- getPostSchedule: "post_schedule",
37
- getReadyPost: "ready_post",
38
- getTemplateRelease: "template_release",
39
- getTemplateSource: "template_source",
40
- getTemplateSourceByLocation: "template_source",
41
- getTemplateSourceBySlugId: "template_source",
42
- getTemplateSourceByTemplateId: "template_source",
43
- getUserAttachment: "user_attachment",
44
- getUserTemporaryFile: "user_temporary_file",
45
- listEmailChannels: "email_channel",
46
- listPostSchedulesForCalendar: "post_schedule",
47
- listPostSchedulesForPost: "post_schedule",
48
- listReadyPosts: "ready_post",
49
- listTemplateReleases: "template_release",
50
- listTemplateSources: "template_source",
51
- listUserAttachments: "user_attachment",
52
- listUserFileFolders: "user_file_folder",
53
- listUserTemporaryFiles: "user_temporary_file",
54
- markPostScheduleCancelled: "post_schedule",
55
- markPostScheduleFailed: "post_schedule",
56
- moveUserAttachmentFolder: "user_attachment",
57
- moveUserFileFolder: "user_file_folder",
58
- moveUserTemporaryFolder: "user_temporary_file",
59
- queueWebhookDelivery: "webhook_delivery",
60
- setReadyPostZip: "ready_post",
61
- updateEmailChannel: "email_channel",
62
- updateEmailChannelAvatar: "email_channel",
63
- updateEmailChannelVerification: "email_channel",
64
- updatePostScheduleProvider: "post_schedule",
65
- updateTemplateSource: "template_source",
66
- upsertExternalPostSchedule: "post_schedule"
67
- };
68
- class ServerlessRecordsServiceImpl {
69
- auth = new ServerlessAuthService();
70
- async getCustomerById(customerId) {
71
- return this.auth.getCustomerById(customerId);
72
- }
73
- async getCustomerByEmail(email) {
74
- return this.auth.getCustomerByEmailPublic(email);
75
- }
76
- async upsertCustomer(input) {
77
- return this.auth.upsertCustomer(input);
78
- }
79
- async insertApiKey(input) {
80
- return this.auth.insertApiKey(input);
81
- }
82
- async getFirstPaidCustomer() {
83
- const response = await dynamodb.send(new ScanCommand({
84
- TableName: requireMainTableName(),
85
- FilterExpression: "entity_type = :type AND is_paid_plan = :paid",
86
- ExpressionAttributeValues: {
87
- ":type": "Customer",
88
- ":paid": true
89
- },
90
- Limit: 1
91
- }));
92
- return response.Items?.[0] ? itemToCustomer(response.Items[0]) : null;
93
- }
94
- async setCustomerAccessByEmail(input) {
95
- const normalizedEmail = input.email.trim().toLowerCase();
96
- const existing = await this.auth.getCustomerByEmailPublic(normalizedEmail);
97
- return this.auth.upsertCustomer({
98
- id: existing?.id ?? createId("cus"),
99
- email: normalizedEmail,
100
- name: input.name?.trim() || existing?.name || displayNameFromEmail(normalizedEmail),
101
- defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
102
- isDeveloper: input.isDeveloper ?? existing?.isDeveloper ?? false,
103
- isAgency: input.isAgency ?? existing?.isAgency ?? false,
104
- isPaidPlan: input.isPaidPlan ?? existing?.isPaidPlan ?? false,
105
- planTier: input.planTier ?? existing?.planTier ?? "client"
106
- });
107
- }
108
- async updateCustomerPaidPlan(customerId, isPaidPlan, planTier = "client") {
109
- const existing = await this.auth.getCustomerById(customerId);
110
- if (!existing)
111
- return null;
112
- return this.auth.upsertCustomer({ ...existing, isPaidPlan, planTier });
113
- }
114
- async updateCustomerAgencyStatus(customerId, isAgency) {
115
- const existing = await this.auth.getCustomerById(customerId);
116
- if (!existing)
117
- return null;
118
- return this.auth.upsertCustomer({ ...existing, isAgency, planTier: isAgency ? "agency" : existing.planTier });
119
- }
120
- async updateCustomerProfile(input) {
121
- const existing = await this.auth.getCustomerById(input.customerId);
122
- if (!existing)
123
- return null;
124
- return this.auth.upsertCustomer({
125
- ...existing,
126
- name: input.name === undefined ? existing.name : input.name,
127
- about: input.about === undefined ? existing.about : input.about,
128
- defaultWebhookUrl: input.defaultWebhookUrl === undefined ? existing.defaultWebhookUrl : input.defaultWebhookUrl,
129
- groupchatUrl: input.groupchatUrl === undefined ? existing.groupchatUrl : input.groupchatUrl,
130
- flockposterApiKey: input.flockposterApiKey === undefined ? existing.flockposterApiKey : input.flockposterApiKey
131
- });
132
- }
133
- async getLatestApiKeyForCustomer(customerId) {
134
- const response = await dynamodb.send(new QueryCommand({
135
- TableName: requireMainTableName(),
136
- IndexName: "gsi1",
137
- KeyConditionExpression: "gsi1pk = :pk",
138
- FilterExpression: "#status = :status",
139
- ExpressionAttributeNames: {
140
- "#status": "status"
141
- },
142
- ExpressionAttributeValues: {
143
- ":pk": `CUSTOMER#${customerId}#APIKEYS`,
144
- ":status": "active"
145
- },
146
- ScanIndexForward: false,
147
- Limit: 1
148
- }));
149
- return response.Items?.[0] ?? null;
150
- }
151
- async hasCustomerCreditGrant(customerId, grantType) {
152
- return Boolean(await this.getById("credit_grant", `${customerId}:${grantType}`));
153
- }
154
- async insertCustomerCreditGrant(input) {
155
- await this.putRecord("credit_grant", {
156
- ...input,
157
- id: `${input.customerId}:${input.grantType}`,
158
- customerId: input.customerId
159
- });
160
- }
161
- async grantAgencyClientAccess(input) {
162
- return this.putRecord("agency_access", {
163
- id: `${input.agencyCustomerId}:${input.clientCustomerId}`,
164
- customerId: input.agencyCustomerId,
165
- agencyCustomerId: input.agencyCustomerId,
166
- clientCustomerId: input.clientCustomerId
167
- });
168
- }
169
- async revokeAgencyClientAccess(input) {
170
- const record = await this.getById("agency_access", `${input.agencyCustomerId}:${input.clientCustomerId}`);
171
- await this.deleteById("agency_access", `${input.agencyCustomerId}:${input.clientCustomerId}`);
172
- return record;
173
- }
174
- async listAgencyClients(input) {
175
- const links = await this.listByCustomer("agency_access", input.agencyCustomerId);
176
- const clients = [];
177
- for (const link of links) {
178
- const clientId = String(link.clientCustomerId ?? "");
179
- const customer = clientId ? await this.auth.getCustomerById(clientId) : null;
180
- if (customer)
181
- clients.push({ ...link, customer });
182
- }
183
- return clients;
184
- }
185
- async getAgencyClient(agencyCustomerId, clientCustomerId) {
186
- return this.getById("agency_access", `${agencyCustomerId}:${clientCustomerId}`);
187
- }
188
- async createCompositionFork(input) {
189
- const timestamp = nowIso();
190
- return this.putRecord("composition_fork", {
191
- id: input.id ?? createIdV7("fork"),
192
- customerId: input.customerId,
193
- sourceSlugId: input.sourceSlugId,
194
- sourceVideoId: input.sourceVideoId ?? null,
195
- parentForkId: input.parentForkId ?? null,
196
- parentVersion: input.parentVersion ?? null,
197
- title: input.title ?? null,
198
- visibility: input.visibility ?? "private",
199
- latestVersion: 0,
200
- currentRenderJobId: null,
201
- deletedAt: null,
202
- createdAt: timestamp,
203
- updatedAt: timestamp
204
- });
205
- }
206
- async getCompositionFork(forkId) {
207
- const fork = await this.getById("composition_fork", forkId);
208
- if (!fork)
209
- return null;
210
- return {
211
- ...fork,
212
- sourceVideoId: fork.sourceVideoId ?? null,
213
- parentForkId: fork.parentForkId ?? null,
214
- parentVersion: fork.parentVersion ?? null,
215
- visibility: fork.visibility ?? "private",
216
- latestVersion: fork.latestVersion ?? 0
217
- };
218
- }
219
- async listCompositionForksForCustomer(customerId) {
220
- return this.listByCustomer("composition_fork", customerId);
221
- }
222
- async listPublicCompositionForks() {
223
- return (await this.scanCollection("composition_fork"))
224
- .filter((fork) => !fork.deletedAt && fork.visibility === "public");
225
- }
226
- async updateCompositionFork(input) {
227
- const existing = await this.getCompositionFork(input.forkId);
228
- if (!existing)
229
- return null;
230
- return this.putRecord("composition_fork", {
231
- ...existing,
232
- ...input.patch,
233
- id: existing.id,
234
- customerId: existing.customerId,
235
- updatedAt: nowIso()
236
- });
237
- }
238
- async deleteCompositionFork(input) {
239
- const existing = await this.getCompositionFork(input.forkId);
240
- if (!existing || existing.customerId !== input.customerId)
241
- return null;
242
- return this.putRecord("composition_fork", {
243
- ...existing,
244
- deletedAt: nowIso(),
245
- updatedAt: nowIso()
246
- });
247
- }
248
- async grantForkPermission(input) {
249
- const existing = (await this.listForkPermissions(input.forkId))
250
- .find((perm) => perm.granteeType === input.granteeType && perm.granteeId === input.granteeId);
251
- const timestamp = nowIso();
252
- const id = existing?.id ?? createId("perm");
253
- return this.putRecord("fork_permission", {
254
- id,
255
- forkId: input.forkId,
256
- ownerCustomerId: input.ownerCustomerId,
257
- granteeType: input.granteeType,
258
- granteeId: input.granteeId,
259
- role: input.role,
260
- grantedBy: input.grantedBy,
261
- expiresAt: input.expiresAt ?? null,
262
- createdAt: existing?.createdAt ?? timestamp,
263
- updatedAt: timestamp
264
- }, { partitionKey: forkPartitionPk("fork_permission", input.forkId) });
265
- }
266
- async listForkPermissions(forkId) {
267
- return this.listByPartition("fork_permission", forkPartitionPk("fork_permission", forkId));
268
- }
269
- async listForkPermissionsForGrantee(customerId) {
270
- return (await this.scanCollection("fork_permission"))
271
- .filter((perm) => perm.granteeType === "customer" && perm.granteeId === customerId);
272
- }
273
- async deleteForkPermission(input) {
274
- const existing = (await this.listForkPermissions(input.forkId)).find((perm) => perm.id === input.permissionId);
275
- if (!existing)
276
- return null;
277
- await this.deleteById("fork_permission", input.permissionId);
278
- return existing;
279
- }
280
- async createForkShareLink(input) {
281
- const timestamp = nowIso();
282
- return this.putRecord("fork_share_link", {
283
- id: input.token,
284
- forkId: input.forkId,
285
- ownerCustomerId: input.ownerCustomerId,
286
- token: input.token,
287
- role: input.role,
288
- createdBy: input.createdBy,
289
- expiresAt: input.expiresAt ?? null,
290
- revokedAt: null,
291
- createdAt: timestamp,
292
- updatedAt: timestamp
293
- }, { partitionKey: forkPartitionPk("fork_share_link", input.forkId) });
294
- }
295
- async getForkShareLinkByToken(token) {
296
- return this.getById("fork_share_link", token);
297
- }
298
- async listForkShareLinks(forkId) {
299
- return this.listByPartition("fork_share_link", forkPartitionPk("fork_share_link", forkId));
300
- }
301
- async revokeForkShareLink(input) {
302
- const existing = await this.getForkShareLinkByToken(input.token);
303
- if (!existing || existing.forkId !== input.forkId)
304
- return null;
305
- return this.putRecord("fork_share_link", {
306
- ...existing,
307
- revokedAt: nowIso(),
308
- updatedAt: nowIso()
309
- }, { partitionKey: forkPartitionPk("fork_share_link", input.forkId) });
310
- }
311
- async createForkVersion(input) {
312
- const timestamp = nowIso();
313
- return this.putRecord("fork_version", {
314
- id: `${input.forkId}:${input.version}`,
315
- forkId: input.forkId,
316
- ownerCustomerId: input.ownerCustomerId,
317
- version: input.version,
318
- reason: input.reason,
319
- message: input.message ?? null,
320
- createdBy: input.createdBy,
321
- storagePrefix: input.storagePrefix,
322
- renderJobId: input.renderJobId ?? null,
323
- createdAt: timestamp,
324
- updatedAt: timestamp
325
- }, { partitionKey: forkPartitionPk("fork_version", input.forkId) });
326
- }
327
- async getForkVersion(forkId, version) {
328
- return this.getById("fork_version", `${forkId}:${version}`);
329
- }
330
- async listForkVersions(input) {
331
- const limit = Math.max(1, Math.min(input.limit ?? 25, 200));
332
- let ExclusiveStartKey = decodeDynamoCursor(input.cursor);
333
- const items = [];
334
- do {
335
- const response = await dynamodb.send(new QueryCommand({
336
- TableName: requireMainTableName(),
337
- IndexName: "gsi1",
338
- KeyConditionExpression: "gsi1pk = :pk",
339
- ExpressionAttributeValues: {
340
- ":pk": forkPartitionPk("fork_version", input.forkId)
341
- },
342
- ScanIndexForward: false,
343
- Limit: Math.max(1, limit - items.length),
344
- ExclusiveStartKey
345
- }));
346
- items.push(...(response.Items ?? []).map((item) => item.data));
347
- ExclusiveStartKey = response.LastEvaluatedKey;
348
- } while (ExclusiveStartKey && items.length < limit);
349
- return { items, nextCursor: encodeDynamoCursor(ExclusiveStartKey) };
350
- }
351
- async createSubmittedVideo(input) {
352
- const timestamp = nowIso();
353
- return this.putRecord("submitted_video", {
354
- id: input.id ?? createId("video"),
355
- customerId: input.customerId,
356
- sourceUrl: input.sourceUrl,
357
- sourceHost: input.sourceHost,
358
- status: "downloading",
359
- downloadJobId: input.downloadJobId ?? null,
360
- videoStorageKey: null,
361
- videoUrl: null,
362
- thumbnailUrl: null,
363
- title: null,
364
- durationSeconds: null,
365
- defaultForkId: null,
366
- errorMessage: null,
367
- createdAt: timestamp,
368
- updatedAt: timestamp
369
- });
370
- }
371
- async getSubmittedVideo(videoId) {
372
- return this.getById("submitted_video", videoId);
373
- }
374
- async findSubmittedVideoByUrl(sourceUrl) {
375
- return this.findByScan("submitted_video", (item) => item.sourceUrl === sourceUrl);
376
- }
377
- async updateSubmittedVideo(input) {
378
- const existing = await this.getSubmittedVideo(input.videoId);
379
- if (!existing)
380
- return null;
381
- return this.putRecord("submitted_video", {
382
- ...existing,
383
- ...input.patch,
384
- id: existing.id,
385
- customerId: existing.customerId,
386
- updatedAt: nowIso()
387
- });
388
- }
389
- async listSubmittedVideosForCustomer(customerId) {
390
- return this.listByCustomer("submitted_video", customerId);
391
- }
392
- async listSubmittedVideosFeed(input = {}) {
393
- const limit = Math.max(1, Math.min(input.limit ?? 50, 200));
394
- const all = (await this.scanCollection("submitted_video"))
395
- .filter((video) => video.status === "ready");
396
- return all
397
- .sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")))
398
- .slice(0, limit);
399
- }
400
- async createReadyPost(input) {
401
- const timestamp = nowIso();
402
- return this.putRecord("ready_post", {
403
- id: input.id ?? createId("post"),
404
- customerId: input.customerId,
405
- status: input.status ?? "ready",
406
- tracer: input.tracer ?? null,
407
- title: input.title ?? null,
408
- caption: input.caption ?? "",
409
- pinnedComment: input.pinnedComment ?? null,
410
- mediaAssets: input.mediaAssets ?? [],
411
- sharePasswordHash: input.sharePasswordHash ?? null,
412
- zipStorageKey: input.zipStorageKey ?? null,
413
- zipPublicUrl: input.zipPublicUrl ?? null,
414
- archivedAt: input.archivedAt ?? null,
415
- deletedAt: input.deletedAt ?? null,
416
- createdAt: input.createdAt ?? timestamp,
417
- updatedAt: timestamp
418
- });
419
- }
420
- async getReadyPost(postId) {
421
- return this.getById("ready_post", postId);
422
- }
423
- async listReadyPosts(customerId) {
424
- return this.listByCustomer("ready_post", customerId);
425
- }
426
- async archiveReadyPost(customerId, postId) {
427
- const post = await this.getReadyPost(postId);
428
- if (!post || post.customerId !== customerId)
429
- return null;
430
- return this.putRecord("ready_post", { ...post, status: "archived", archivedAt: nowIso(), updatedAt: nowIso() });
431
- }
432
- async deleteReadyPost(customerId, postId) {
433
- const post = await this.getReadyPost(postId);
434
- if (!post || post.customerId !== customerId)
435
- return null;
436
- const updated = await this.putRecord("ready_post", { ...post, status: "deleted", deletedAt: nowIso(), updatedAt: nowIso() });
437
- return updated;
438
- }
439
- async setReadyPostZip(input) {
440
- const post = await this.getReadyPost(input.postId);
441
- if (!post || post.customerId !== input.customerId)
442
- return null;
443
- return this.putRecord("ready_post", {
444
- ...post,
445
- zipStorageKey: input.zipStorageKey,
446
- zipPublicUrl: input.zipPublicUrl,
447
- updatedAt: nowIso()
448
- });
449
- }
450
- async createPostSchedule(input) {
451
- const timestamp = nowIso();
452
- return this.putRecord("post_schedule", {
453
- id: input.id ?? createId("sched"),
454
- customerId: input.customerId,
455
- readyPostId: input.readyPostId ?? null,
456
- managedBy: input.managedBy ?? "vidfarm",
457
- provider: input.provider ?? "flockposter",
458
- destinationType: input.destinationType ?? "flockposter",
459
- destinationId: input.destinationId ?? "",
460
- scheduledAt: input.scheduledAt ?? timestamp,
461
- timezone: input.timezone ?? null,
462
- status: input.status ?? "scheduled",
463
- providerStatus: input.providerStatus ?? null,
464
- providerScheduleId: input.providerScheduleId ?? null,
465
- title: input.title ?? null,
466
- summary: input.summary ?? null,
467
- url: input.url ?? null,
468
- additionalNotes: input.additionalNotes ?? null,
469
- providerPayload: input.providerPayload ?? {},
470
- providerResponse: input.providerResponse ?? {},
471
- lastSyncedAt: input.lastSyncedAt ?? null,
472
- lastError: input.lastError ?? null,
473
- cancelledAt: input.cancelledAt ?? null,
474
- sentAt: input.sentAt ?? null,
475
- createdAt: input.createdAt ?? timestamp,
476
- updatedAt: timestamp
477
- });
478
- }
479
- async getPostSchedule(customerId, scheduleId) {
480
- const schedule = await this.getById("post_schedule", scheduleId);
481
- return schedule?.customerId === customerId ? schedule : null;
482
- }
483
- async listPostSchedulesForPost(customerId, readyPostId) {
484
- return (await this.listByCustomer("post_schedule", customerId)).filter((schedule) => schedule.readyPostId === readyPostId);
485
- }
486
- async listPostSchedulesForCalendar(input) {
487
- return (await this.listByCustomer("post_schedule", input.customerId)).filter((schedule) => {
488
- const scheduledAt = String(schedule.scheduledAt ?? "");
489
- if (input.startTime && scheduledAt < input.startTime)
490
- return false;
491
- if (input.endTime && scheduledAt > input.endTime)
492
- return false;
493
- return true;
494
- });
495
- }
496
- async updatePostScheduleProvider(input) {
497
- return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, input);
498
- }
499
- async markPostScheduleCancelled(input) {
500
- return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, {
501
- ...input,
502
- status: "cancelled",
503
- cancelledAt: nowIso()
504
- });
505
- }
506
- async markPostScheduleFailed(input) {
507
- return this.mergeByIdForCustomer("post_schedule", input.customerId, input.scheduleId, {
508
- ...input,
509
- status: "failed",
510
- lastError: input.lastError ?? "Schedule failed"
511
- });
512
- }
513
- async upsertExternalPostSchedule(input) {
514
- const existing = (await this.listByCustomer("post_schedule", input.customerId))
515
- .find((schedule) => schedule.providerScheduleId === input.providerScheduleId);
516
- return this.createPostSchedule({ ...existing, ...input, managedBy: "external" });
517
- }
518
- async createEmailChannel(input) {
519
- const timestamp = nowIso();
520
- return this.putRecord("email_channel", {
521
- id: input.id ?? createId("email"),
522
- customerId: input.customerId,
523
- title: input.title ?? input.email,
524
- email: input.email,
525
- note: input.note ?? null,
526
- avatarUrl: input.avatarUrl ?? null,
527
- avatarStorageKey: input.avatarStorageKey ?? null,
528
- status: input.status ?? "unverified",
529
- verificationTokenHash: input.verificationTokenHash ?? null,
530
- verificationSentAt: input.verificationSentAt ?? null,
531
- verifiedAt: input.verifiedAt ?? null,
532
- createdAt: input.createdAt ?? timestamp,
533
- updatedAt: timestamp
534
- });
535
- }
536
- async listEmailChannels(customerId) {
537
- return this.listByCustomer("email_channel", customerId);
538
- }
539
- async getEmailChannel(customerId, channelId) {
540
- const channel = await this.getById("email_channel", channelId);
541
- return channel?.customerId === customerId ? channel : null;
542
- }
543
- async getEmailChannelByEmail(customerId, email) {
544
- return (await this.listEmailChannels(customerId)).find((channel) => channel.email.toLowerCase() === email.toLowerCase()) ?? null;
545
- }
546
- async updateEmailChannel(input) {
547
- return this.mergeByIdForCustomer("email_channel", input.customerId, input.channelId, input);
548
- }
549
- async updateEmailChannelAvatar(input) {
550
- await this.updateEmailChannel(input);
551
- }
552
- async updateEmailChannelVerification(input) {
553
- return this.updateEmailChannel(input);
554
- }
555
- async verifyEmailChannelByTokenHash(tokenHash) {
556
- const response = await dynamodb.send(new ScanCommand({
557
- TableName: requireMainTableName(),
558
- FilterExpression: "entity_type = :type AND verificationTokenHash = :token",
559
- ExpressionAttributeValues: {
560
- ":type": "serverless_record#email_channel",
561
- ":token": tokenHash
562
- },
563
- Limit: 1
564
- }));
565
- const channel = response.Items?.[0]?.data;
566
- if (!channel)
567
- return null;
568
- return this.putRecord("email_channel", {
569
- ...channel,
570
- status: "verified",
571
- verifiedAt: nowIso(),
572
- verificationTokenHash: null,
573
- updatedAt: nowIso()
574
- });
575
- }
576
- async deleteEmailChannel(customerId, channelId) {
577
- const existing = await this.getEmailChannel(customerId, channelId);
578
- if (existing)
579
- await this.deleteById("email_channel", channelId);
580
- }
581
- async createUserAttachment(input) {
582
- const timestamp = nowIso();
583
- return this.putRecord("user_attachment", {
584
- id: input.id ?? createId("att"),
585
- customerId: input.customerId,
586
- fileName: input.fileName,
587
- contentType: input.contentType ?? "application/octet-stream",
588
- sizeBytes: input.sizeBytes ?? 0,
589
- storageKey: input.storageKey,
590
- folderPath: normalizeFolder(input.folderPath),
591
- publicUrl: input.publicUrl ?? null,
592
- createdAt: input.createdAt ?? timestamp
593
- });
594
- }
595
- async listUserAttachments(customerId) {
596
- return this.listByCustomer("user_attachment", customerId);
597
- }
598
- async getUserAttachment(customerId, attachmentId) {
599
- const attachment = await this.getById("user_attachment", attachmentId);
600
- return attachment?.customerId === customerId ? attachment : null;
601
- }
602
- async deleteUserAttachment(customerId, attachmentId) {
603
- const existing = await this.getUserAttachment(customerId, attachmentId);
604
- if (existing)
605
- await this.deleteById("user_attachment", attachmentId);
606
- }
607
- async createUserTemporaryFile(input) {
608
- const timestamp = nowIso();
609
- return this.putRecord("user_temporary_file", {
610
- id: input.id ?? createId("tmp"),
611
- customerId: input.customerId,
612
- fileName: input.fileName,
613
- contentType: input.contentType ?? "application/octet-stream",
614
- sizeBytes: input.sizeBytes ?? 0,
615
- storageKey: input.storageKey,
616
- folderPath: normalizeFolder(input.folderPath),
617
- expiresAt: input.expiresAt ?? timestamp,
618
- createdAt: input.createdAt ?? timestamp
619
- });
620
- }
621
- async listUserTemporaryFiles(customerId) {
622
- return this.listByCustomer("user_temporary_file", customerId);
623
- }
624
- async getUserTemporaryFile(customerId, fileId) {
625
- const file = await this.getById("user_temporary_file", fileId);
626
- return file?.customerId === customerId ? file : null;
627
- }
628
- async deleteUserTemporaryFile(customerId, fileId) {
629
- const existing = await this.getUserTemporaryFile(customerId, fileId);
630
- if (existing)
631
- await this.deleteById("user_temporary_file", fileId);
632
- }
633
- async deleteExpiredUserTemporaryFiles() {
634
- const now = nowIso();
635
- const files = (await this.scanCollection("user_temporary_file"))
636
- .filter(isExpiredUserTemporaryFileRecord);
637
- const expired = files.filter((file) => file.expiresAt <= now);
638
- for (const file of expired)
639
- await this.deleteById("user_temporary_file", file.id);
640
- return expired;
641
- }
642
- async createUserFileFolder(customerId, scope, folderPath) {
643
- const normalized = normalizeFolder(folderPath);
644
- await this.putRecord("user_file_folder", {
645
- id: `${customerId}:${scope}:${normalized}`,
646
- customerId,
647
- scope,
648
- folderPath: normalized
649
- });
650
- }
651
- async listUserFileFolders(customerId, scope) {
652
- return (await this.listByCustomer("user_file_folder", customerId))
653
- .filter((folder) => folder.scope === scope)
654
- .map((folder) => String(folder.folderPath));
655
- }
656
- async moveUserFileFolder(customerId, scope, fromFolder, toFolder) {
657
- await this.deleteUserFileFolder(customerId, scope, fromFolder);
658
- await this.createUserFileFolder(customerId, scope, toFolder);
659
- }
660
- async deleteUserFileFolder(customerId, scope, folderPath) {
661
- await this.deleteById("user_file_folder", `${customerId}:${scope}:${normalizeFolder(folderPath)}`);
662
- }
663
- async moveUserAttachmentFolder(customerId, fromFolder, toFolder) {
664
- await this.moveCollectionFolder("user_attachment", customerId, fromFolder, toFolder);
665
- }
666
- async deleteUserAttachmentFolder(customerId, folderPath) {
667
- return this.deleteCollectionFolder("user_attachment", customerId, folderPath);
668
- }
669
- async moveUserTemporaryFolder(customerId, fromFolder, toFolder) {
670
- await this.moveCollectionFolder("user_temporary_file", customerId, fromFolder, toFolder);
671
- }
672
- async deleteUserTemporaryFolder(customerId, folderPath) {
673
- return this.deleteCollectionFolder("user_temporary_file", customerId, folderPath);
674
- }
675
- async queueWebhookDelivery(input) {
676
- const timestamp = nowIso();
677
- await this.putRecord("webhook_delivery", {
678
- id: typeof input.id === "string" ? input.id : createId("webhook"),
679
- customerId: typeof input.customerId === "string" ? input.customerId : undefined,
680
- jobId: input.jobId,
681
- destinationUrl: input.destinationUrl,
682
- destination_url: input.destinationUrl,
683
- eventType: input.eventType,
684
- event_type: input.eventType,
685
- payload: input.payload,
686
- payload_json: JSON.stringify(input.payload ?? {}),
687
- status: "pending",
688
- attemptCount: 0,
689
- attempt_count: 0,
690
- nextAttemptAt: timestamp,
691
- next_attempt_at: timestamp,
692
- createdAt: timestamp,
693
- updatedAt: timestamp
694
- });
695
- }
696
- async listPendingWebhookDeliveries(limit = 100) {
697
- const response = await dynamodb.send(new ScanCommand({
698
- TableName: requireMainTableName(),
699
- FilterExpression: "entity_type = :type AND #data.#status = :status",
700
- ExpressionAttributeNames: { "#data": "data", "#status": "status" },
701
- ExpressionAttributeValues: {
702
- ":type": "serverless_record#webhook_delivery",
703
- ":status": "pending"
704
- },
705
- Limit: limit
706
- }));
707
- return (response.Items ?? []).map((item) => item.data);
708
- }
709
- async markWebhookDelivery(input) {
710
- const existing = await this.getById("webhook_delivery", input.id);
711
- if (!existing)
712
- return;
713
- await this.putRecord("webhook_delivery", {
714
- ...existing,
715
- ...input,
716
- attempt_count: input.attemptCount ?? input.attempt_count ?? existing.attempt_count,
717
- next_attempt_at: input.nextAttemptAt ?? input.next_attempt_at ?? existing.next_attempt_at,
718
- updatedAt: nowIso()
719
- });
720
- }
721
- async listChatThreadMetas(input) {
722
- const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
723
- return (await this.listByCustomer("chat_thread", input.customerId))
724
- .filter((thread) => !input.templateId || thread.templateId === input.templateId)
725
- .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))
726
- .slice(0, limit);
727
- }
728
- async getChatThreadMeta(customerId, threadId) {
729
- const thread = await this.getById("chat_thread", threadId);
730
- return thread?.customerId === customerId ? thread : null;
731
- }
732
- async appendChatThreadMessages(input) {
733
- const existing = await this.getChatThreadMeta(input.customerId, input.threadId);
734
- const timestamp = nowIso();
735
- const thread = {
736
- id: input.threadId,
737
- customerId: input.customerId,
738
- templateId: input.templateId,
739
- title: input.title,
740
- storagePrefix: input.storagePrefix,
741
- tracers: input.tracers,
742
- messageCount: (existing?.messageCount ?? 0) + input.messageCountDelta,
743
- lastMessageAt: input.lastMessageAt,
744
- hiddenFromView: existing?.hiddenFromView ?? false,
745
- archivedAt: existing?.archivedAt ?? null,
746
- createdAt: existing?.createdAt ?? timestamp,
747
- updatedAt: timestamp,
748
- messages: []
749
- };
750
- return this.putRecord("chat_thread", thread);
751
- }
752
- async updateChatThreadState(input) {
753
- const existing = await this.getChatThreadMeta(input.customerId, input.threadId);
754
- if (!existing)
755
- return null;
756
- return this.putRecord("chat_thread", {
757
- ...existing,
758
- title: input.title ?? existing.title,
759
- tracers: input.tracers ?? existing.tracers,
760
- hiddenFromView: input.hiddenFromView ?? existing.hiddenFromView,
761
- archivedAt: input.archivedAt === undefined ? existing.archivedAt : input.archivedAt,
762
- updatedAt: nowIso()
763
- });
764
- }
765
- async deleteChatThread(customerId, threadId) {
766
- const existing = await this.getChatThreadMeta(customerId, threadId);
767
- if (!existing)
768
- return null;
769
- await this.deleteById("chat_thread", threadId);
770
- return existing;
771
- }
772
- async createTemplateSource(input) {
773
- return this.putRecord("template_source", { id: input.id ?? createId("tsrc"), ...input });
774
- }
775
- async updateTemplateSource(input) {
776
- const existing = await this.getById("template_source", String(input.id));
777
- return this.putRecord("template_source", { ...existing, ...input, updatedAt: nowIso() });
778
- }
779
- async getTemplateSource(sourceId) {
780
- return this.getById("template_source", sourceId);
781
- }
782
- async getTemplateSourceByTemplateId(templateId) {
783
- return this.findByScan("template_source", (item) => item.templateId === templateId);
784
- }
785
- async getTemplateSourceBySlugId(slugId) {
786
- return this.findByScan("template_source", (item) => item.slugId === slugId);
787
- }
788
- async getTemplateSourceByLocation(repoUrl, branch, templateModulePath) {
789
- return this.findByScan("template_source", (item) => item.repoUrl === repoUrl && item.branch === branch && item.templateModulePath === templateModulePath);
790
- }
791
- async listTemplateSources() {
792
- return this.scanCollection("template_source");
793
- }
794
- async createTemplateRelease(input) {
795
- return this.putRecord("template_release", { id: input.id ?? createId("trel"), ...input });
796
- }
797
- async getTemplateRelease(releaseId) {
798
- return this.getById("template_release", releaseId);
799
- }
800
- async getTemplateReleaseBySourceAndCommit(sourceId, commitSha) {
801
- return this.findByScan("template_release", (item) => item.sourceId === sourceId && item.commitSha === commitSha);
802
- }
803
- async getLatestTemplateReleaseForSource(sourceId) {
804
- return (await this.scanCollection("template_release"))
805
- .filter((release) => release.sourceId === sourceId)
806
- .sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")))[0] ?? null;
807
- }
808
- async listTemplateReleases() {
809
- return this.scanCollection("template_release");
810
- }
811
- async listActiveTemplateReleases() {
812
- const releases = [];
813
- let cursor = null;
814
- do {
815
- const page = await this.listActiveTemplateReleasePage({ cursor, limit: 500 });
816
- releases.push(...page.items);
817
- cursor = page.nextCursor;
818
- } while (cursor);
819
- return releases;
820
- }
821
- async listActiveTemplateReleasePage(input = {}) {
822
- const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
823
- const releases = [];
824
- let ExclusiveStartKey = decodeDynamoCursor(input.cursor);
825
- do {
826
- const response = await dynamodb.send(new QueryCommand({
827
- TableName: requireMainTableName(),
828
- IndexName: "gsi1",
829
- KeyConditionExpression: "gsi1pk = :pk",
830
- FilterExpression: "#data.#record_kind = :kind",
831
- ExpressionAttributeNames: {
832
- "#data": "data",
833
- "#record_kind": "recordKind"
834
- },
835
- ExpressionAttributeValues: {
836
- ":pk": collectionPk("active_template"),
837
- ":kind": "active_template_release"
838
- },
839
- ScanIndexForward: false,
840
- Limit: Math.max(1, limit - releases.length),
841
- ExclusiveStartKey
842
- }));
843
- releases.push(...(response.Items ?? []).map((item) => item.data));
844
- ExclusiveStartKey = response.LastEvaluatedKey;
845
- } while (ExclusiveStartKey && releases.length < limit);
846
- return {
847
- items: releases,
848
- nextCursor: encodeDynamoCursor(ExclusiveStartKey)
849
- };
850
- }
851
- async getActiveTemplateReleaseByTemplateId(templateId) {
852
- const direct = await this.getById("active_template", templateId);
853
- if (direct?.recordKind === "active_template_release") {
854
- return direct;
855
- }
856
- const alias = await this.getById("active_template", activeTemplateSlugAliasId(templateId));
857
- if (alias?.recordKind === "active_template_slug_alias" && typeof alias.templateId === "string") {
858
- const release = await this.getById("active_template", alias.templateId);
859
- return release?.recordKind === "active_template_release"
860
- ? release
861
- : null;
862
- }
863
- return null;
864
- }
865
- async putActiveTemplateRelease(release) {
866
- const slugId = release.templateSnapshot?.slugId;
867
- const data = await this.putRecord("active_template", {
868
- ...release,
869
- id: release.templateId,
870
- recordKind: "active_template_release",
871
- updatedAt: release.activatedAt ?? release.updatedAt ?? nowIso()
872
- });
873
- if (slugId) {
874
- await this.putRecord("active_template", {
875
- id: activeTemplateSlugAliasId(slugId),
876
- recordKind: "active_template_slug_alias",
877
- templateId: release.templateId,
878
- slugId,
879
- releaseId: release.id,
880
- updatedAt: release.activatedAt ?? release.updatedAt ?? nowIso()
881
- });
882
- }
883
- return data;
884
- }
885
- async updateTemplateReleaseStatus(input) {
886
- const existing = await this.getById("template_release", input.releaseId);
887
- if (existing)
888
- await this.putRecord("template_release", { ...existing, ...input, id: input.releaseId, updatedAt: nowIso() });
889
- }
890
- async clearActiveTemplateReleases(templateId) {
891
- const active = await this.getActiveTemplateReleaseByTemplateId(templateId);
892
- if (active) {
893
- await this.putRecord("template_release", { ...active, id: String(active.id), status: "approved", activatedAt: null, updatedAt: nowIso() });
894
- await this.deleteById("active_template", active.templateId);
895
- if (active.templateSnapshot?.slugId) {
896
- await this.deleteById("active_template", activeTemplateSlugAliasId(active.templateSnapshot.slugId));
897
- }
898
- }
899
- }
900
- async call(method, args) {
901
- const collection = METHOD_COLLECTIONS[method];
902
- if (!collection) {
903
- throw new Error(`Serverless record method ${method} is not implemented.`);
904
- }
905
- devLog("serverless_records.dynamic_call", { method, collection });
906
- throw new Error(`Serverless record method ${method} requires a typed implementation.`);
907
- }
908
- async mergeByIdForCustomer(collection, customerId, id, patch) {
909
- const existing = await this.getById(collection, id);
910
- if (!existing || existing.customerId !== customerId)
911
- return null;
912
- return this.putRecord(collection, { ...existing, ...patch, id, customerId, updatedAt: nowIso() });
913
- }
914
- async moveCollectionFolder(collection, customerId, fromFolder, toFolder) {
915
- for (const record of await this.listByCustomer(collection, customerId)) {
916
- if (normalizeFolder(String(record.folderPath ?? "")) === normalizeFolder(fromFolder)) {
917
- await this.putRecord(collection, { ...record, folderPath: normalizeFolder(toFolder), updatedAt: nowIso() });
918
- }
919
- }
920
- }
921
- async deleteCollectionFolder(collection, customerId, folderPath) {
922
- const deleted = [];
923
- for (const record of await this.listByCustomer(collection, customerId)) {
924
- if (normalizeFolder(String(record.folderPath ?? "")) === normalizeFolder(folderPath)) {
925
- await this.deleteById(collection, String(record.id));
926
- deleted.push(record);
927
- }
928
- }
929
- return deleted;
930
- }
931
- async findByScan(collection, predicate) {
932
- return (await this.scanCollection(collection)).find(predicate) ?? null;
933
- }
934
- async scanCollection(collection) {
935
- const results = [];
936
- let exclusiveStartKey;
937
- // DDB Scan with FilterExpression only reads one page at a time; matching
938
- // items later in the table are silently dropped unless you follow
939
- // LastEvaluatedKey. Paginate until DDB stops returning a cursor.
940
- do {
941
- const response = await dynamodb.send(new ScanCommand({
942
- TableName: requireMainTableName(),
943
- FilterExpression: "entity_type = :type",
944
- ExpressionAttributeValues: {
945
- ":type": entityType(collection)
946
- },
947
- ExclusiveStartKey: exclusiveStartKey
948
- }));
949
- for (const item of response.Items ?? []) {
950
- results.push(item.data);
951
- }
952
- exclusiveStartKey = response.LastEvaluatedKey;
953
- } while (exclusiveStartKey);
954
- return results;
955
- }
956
- async listByCustomer(collection, customerId) {
957
- return this.listByPartition(collection, customerCollectionPk(collection, customerId));
958
- }
959
- async listByPartition(_collection, partitionKey) {
960
- const response = await dynamodb.send(new QueryCommand({
961
- TableName: requireMainTableName(),
962
- IndexName: "gsi1",
963
- KeyConditionExpression: "gsi1pk = :pk",
964
- ExpressionAttributeValues: {
965
- ":pk": partitionKey
966
- },
967
- ScanIndexForward: false
968
- }));
969
- return (response.Items ?? []).map((item) => item.data);
970
- }
971
- async getById(collection, id) {
972
- const response = await dynamodb.send(new GetCommand({
973
- TableName: requireMainTableName(),
974
- Key: recordKey(collection, id)
975
- }));
976
- return response.Item?.data ?? null;
977
- }
978
- async deleteById(collection, id) {
979
- await dynamodb.send(new DeleteCommand({
980
- TableName: requireMainTableName(),
981
- Key: recordKey(collection, id)
982
- }));
983
- devLog("serverless_records.deleted", { collection, id });
984
- }
985
- async putRecord(collection, record, options) {
986
- const timestamp = nowIso();
987
- const id = typeof record.id === "string" && record.id ? record.id : createId("rec");
988
- const data = {
989
- ...record,
990
- id,
991
- createdAt: record.createdAt ?? timestamp,
992
- updatedAt: record.updatedAt ?? timestamp
993
- };
994
- const gsi1pk = options?.partitionKey
995
- ?? (data.customerId ? customerCollectionPk(collection, String(data.customerId)) : collectionPk(collection));
996
- await dynamodb.send(new PutCommand({
997
- TableName: requireMainTableName(),
998
- Item: {
999
- ...recordKey(collection, id),
1000
- entity_type: entityType(collection),
1001
- gsi1pk,
1002
- gsi1sk: `${String(data.updatedAt)}#${id}`,
1003
- data
1004
- }
1005
- }));
1006
- devLog("serverless_records.put", {
1007
- collection,
1008
- id,
1009
- customer_id: data.customerId ?? null,
1010
- partition_key: gsi1pk
1011
- });
1012
- return data;
1013
- }
1014
- }
1015
- function forkPartitionPk(collection, forkId) {
1016
- return `FORK#${forkId}#${collection}`;
1017
- }
1018
- function requireMainTableName() {
1019
- if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
1020
- throw new Error("Serverless record store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
1021
- }
1022
- return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
1023
- }
1024
- function recordKey(collection, id) {
1025
- return {
1026
- pk: `${collectionPk(collection)}#${id}`,
1027
- sk: "META"
1028
- };
1029
- }
1030
- function collectionPk(collection) {
1031
- return `SERVERLESS_RECORD#${collection}`;
1032
- }
1033
- function activeTemplateSlugAliasId(slugId) {
1034
- return `slug#${slugId}`;
1035
- }
1036
- function encodeDynamoCursor(value) {
1037
- return value ? Buffer.from(JSON.stringify(value), "utf8").toString("base64url") : null;
1038
- }
1039
- function decodeDynamoCursor(raw) {
1040
- if (!raw) {
1041
- return undefined;
1042
- }
1043
- try {
1044
- const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
1045
- return parsed && typeof parsed === "object" ? parsed : undefined;
1046
- }
1047
- catch {
1048
- return undefined;
1049
- }
1050
- }
1051
- function customerCollectionPk(collection, customerId) {
1052
- return `CUSTOMER#${customerId}#${collection}`;
1053
- }
1054
- function entityType(collection) {
1055
- return `serverless_record#${collection}`;
1056
- }
1057
- function normalizeFolder(value) {
1058
- if (typeof value !== "string")
1059
- return "";
1060
- return value.trim().replace(/^\/+|\/+$/g, "");
1061
- }
1062
- function isExpiredUserTemporaryFileRecord(record) {
1063
- return typeof record.id === "string"
1064
- && record.id.length > 0
1065
- && typeof record.expiresAt === "string"
1066
- && record.expiresAt.length > 0;
1067
- }
1068
- function itemToCustomer(item) {
1069
- return {
1070
- id: String(item.customer_id),
1071
- email: String(item.email),
1072
- name: typeof item.name === "string" ? item.name : null,
1073
- defaultWebhookUrl: typeof item.default_webhook_url === "string" ? item.default_webhook_url : null,
1074
- isDeveloper: Boolean(item.is_developer),
1075
- isAgency: Boolean(item.is_agency),
1076
- isPaidPlan: Boolean(item.is_paid_plan),
1077
- planTier: item.plan_tier === "standalone" || item.plan_tier === "agency" ? item.plan_tier : "client",
1078
- about: typeof item.about === "string" ? item.about : null,
1079
- groupchatUrl: typeof item.groupchat_url === "string" ? item.groupchat_url : null,
1080
- flockposterApiKey: typeof item.flockposter_api_key === "string" ? item.flockposter_api_key : null,
1081
- createdAt: typeof item.created_at === "string" ? item.created_at : nowIso(),
1082
- updatedAt: typeof item.updated_at === "string" ? item.updated_at : nowIso()
1083
- };
1084
- }
1085
- export const serverlessRecords = new ServerlessRecordsServiceImpl();
1086
- //# sourceMappingURL=serverless-records.js.map