thinkwork-cli 0.13.2 → 1.0.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 (231) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
  3. package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
  4. package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
  5. package/dist/chunk-MWQ7RO76.js +137 -0
  6. package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
  7. package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
  8. package/dist/cli.js +4400 -16431
  9. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  11. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  12. package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
  13. package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
  14. package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
  15. package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
  16. package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
  17. package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
  18. package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
  19. package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
  20. package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
  21. package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
  22. package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
  23. package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
  24. package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
  25. package/dist/drizzle/0230_analyst_rls.sql +28 -45
  26. package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
  27. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  28. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  29. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  30. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  31. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  32. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  33. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  34. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  35. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  36. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  37. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  38. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  39. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  40. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  41. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  42. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  43. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  44. package/dist/drizzle/0287_brain_subject.sql +25 -0
  45. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  46. package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
  47. package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
  48. package/dist/plugins/README.md +2 -2
  49. package/dist/plugins/catalog/package.json +0 -3
  50. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
  51. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  52. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
  53. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
  54. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
  55. package/dist/plugins/catalog/src/contracts.ts +15 -0
  56. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
  57. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  58. package/dist/plugins/twenty/package.json +2 -1
  59. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  60. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  61. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  62. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  63. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  64. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  65. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  66. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  67. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  68. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  69. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  70. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  71. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  72. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  73. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  74. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  75. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  76. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  77. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  78. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  79. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  80. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  81. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  82. package/dist/scripts/post-deploy.sh +79 -1
  83. package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
  84. package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
  85. package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
  86. package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
  87. package/dist/terraform/examples/greenfield/main.tf +233 -175
  88. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
  89. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  90. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  91. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  92. package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
  93. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
  100. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
  101. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  102. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  103. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  104. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  105. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  106. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  107. package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
  108. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  109. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  110. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  111. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  112. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  113. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  114. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  115. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  116. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  117. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  118. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  119. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  120. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  121. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  122. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  123. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  124. package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
  125. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
  126. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  127. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  128. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  129. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  130. package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
  131. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
  132. package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
  133. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
  134. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  135. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  136. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
  137. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  138. package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
  139. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  140. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  141. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  142. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  143. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  144. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  145. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  146. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  147. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  148. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  149. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  150. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  151. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  152. package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
  153. package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
  154. package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
  155. package/dist/terraform/modules/thinkwork/README.md +5 -1
  156. package/dist/terraform/modules/thinkwork/main.tf +376 -374
  157. package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
  158. package/dist/terraform/modules/thinkwork/variables.tf +246 -89
  159. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  160. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  161. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  162. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  163. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  164. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  165. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  166. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  167. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  168. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  169. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  171. package/package.json +1 -1
  172. package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
  173. package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
  174. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
  175. package/dist/plugins/company-data/README.md +0 -49
  176. package/dist/plugins/company-data/package.json +0 -21
  177. package/dist/plugins/company-data/src/index.ts +0 -30
  178. package/dist/plugins/company-data/src/manifest.ts +0 -26
  179. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  180. package/dist/plugins/company-data/tsconfig.json +0 -8
  181. package/dist/plugins/company-etl/README.md +0 -46
  182. package/dist/plugins/company-etl/package.json +0 -21
  183. package/dist/plugins/company-etl/src/index.ts +0 -29
  184. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  185. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  186. package/dist/plugins/company-etl/tsconfig.json +0 -8
  187. package/dist/plugins/lastmile/README.md +0 -50
  188. package/dist/plugins/lastmile/package.json +0 -23
  189. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  190. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  191. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  192. package/dist/plugins/lastmile/src/index.ts +0 -46
  193. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  194. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  195. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  196. package/dist/plugins/lastmile/tsconfig.json +0 -8
  197. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  198. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  199. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  200. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  201. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  202. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  203. package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
  204. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
  205. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
  206. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
  207. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
  208. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
  209. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
  210. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
  211. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
  212. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
  213. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
  214. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
  215. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
  216. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
  217. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
  218. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
  219. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  220. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  221. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
  222. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
  223. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  224. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  225. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  226. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  227. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  228. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  229. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  230. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  231. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Hard-delete everything the LastMile import created in TEI's Twenty, so the
4
+ * task-first import can start from a clean workspace.
5
+ *
6
+ * Two disjoint sets are destroyed:
7
+ * 1. Records carrying a migration `sourceId` (companies, people,
8
+ * opportunities, opportunityProducts, notes, attachments).
9
+ * 2. Companies with NO sourceId whose `createdBy.source` is WORKFLOW — the
10
+ * domain-named companies ("cox.net") that Twenty's stock
11
+ * "Create company when adding a new person" workflow invented on every
12
+ * person insert, mislinking 21,989 of 24,028 migrated people.
13
+ *
14
+ * `destroy*` is permanent (unlike `delete*`, which soft-deletes). Nothing else
15
+ * in the workspace is touched: records without a sourceId that were not
16
+ * workflow-created are never candidates, and neither are workspace members.
17
+ *
18
+ * Usage:
19
+ * npx tsx scripts/purge-lastmile-import.ts # dry-run: counts only
20
+ * npx tsx scripts/purge-lastmile-import.ts --apply # destroy
21
+ */
22
+
23
+ import process from "node:process";
24
+
25
+ import {
26
+ COMPANY,
27
+ NOTE,
28
+ OPPORTUNITY,
29
+ OPPORTUNITY_PRODUCT,
30
+ PERSON,
31
+ type EntityShape,
32
+ } from "./lib/load-records";
33
+ import { chunk, TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
34
+
35
+ const ATTACHMENT: EntityShape = {
36
+ singular: "attachment",
37
+ plural: "attachments",
38
+ capSingular: "Attachment",
39
+ capPlural: "Attachments",
40
+ };
41
+
42
+ /** Children before parents: relations block a parent destroy otherwise. */
43
+ const SOURCE_ID_ENTITIES: EntityShape[] = [
44
+ ATTACHMENT,
45
+ NOTE,
46
+ OPPORTUNITY_PRODUCT,
47
+ OPPORTUNITY,
48
+ PERSON,
49
+ COMPANY,
50
+ ];
51
+
52
+ const PAGE = 100;
53
+ /** Twenty caps a mutation's affected records (clientConfig: 100). */
54
+ const DESTROY_BATCH = 60;
55
+
56
+ function requireEnv(name: string): string {
57
+ const value = process.env[name];
58
+ if (!value) throw new Error(`Missing required environment variable: ${name}`);
59
+ return value;
60
+ }
61
+
62
+ function log(message: string): void {
63
+ process.stderr.write(`${message}\n`);
64
+ }
65
+
66
+ async function collectIds(
67
+ client: TwentyClient,
68
+ entity: EntityShape,
69
+ filter: Record<string, unknown>,
70
+ extraNodeFields = "",
71
+ ): Promise<string[]> {
72
+ const ids: string[] = [];
73
+ let after: string | null = null;
74
+ for (;;) {
75
+ const data: {
76
+ [plural: string]: {
77
+ edges: Array<{ node: { id: string } }>;
78
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
79
+ };
80
+ } = await client.requestWithRetry(
81
+ "/graphql",
82
+ `query PurgeList${entity.capPlural}($filter: ${entity.capSingular}FilterInput, $after: String) {
83
+ ${entity.plural}(filter: $filter, first: ${PAGE}, after: $after) {
84
+ edges { node { id ${extraNodeFields} } cursor }
85
+ pageInfo { hasNextPage endCursor }
86
+ }
87
+ }`,
88
+ { filter, ...(after ? { after } : {}) },
89
+ );
90
+ ids.push(...data[entity.plural].edges.map((edge) => edge.node.id));
91
+ if (!data[entity.plural].pageInfo.hasNextPage) break;
92
+ after = data[entity.plural].pageInfo.endCursor;
93
+ }
94
+ return ids;
95
+ }
96
+
97
+ async function destroyIds(
98
+ client: TwentyClient,
99
+ entity: EntityShape,
100
+ ids: string[],
101
+ ): Promise<{ destroyed: number; failed: string[] }> {
102
+ let destroyed = 0;
103
+ const failed: string[] = [];
104
+ for (const batch of chunk(ids, DESTROY_BATCH)) {
105
+ try {
106
+ await client.requestOnce(
107
+ "/graphql",
108
+ `mutation Purge${entity.capPlural}($filter: ${entity.capSingular}FilterInput!) {
109
+ destroy${entity.capPlural}(filter: $filter) { id }
110
+ }`,
111
+ { filter: { id: { in: batch } } },
112
+ );
113
+ destroyed += batch.length;
114
+ } catch (error) {
115
+ failed.push(
116
+ error instanceof Error ? error.message.slice(0, 200) : String(error),
117
+ );
118
+ }
119
+ }
120
+ return { destroyed, failed };
121
+ }
122
+
123
+ async function main(): Promise<void> {
124
+ const apply = process.argv.slice(2).includes("--apply");
125
+ const client = new TwentyClient({
126
+ baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
127
+ authToken: requireEnv("TWENTY_API_KEY"),
128
+ });
129
+
130
+ const report: Record<string, unknown> = { mode: apply ? "apply" : "dry-run" };
131
+
132
+ for (const entity of SOURCE_ID_ENTITIES) {
133
+ log(`scanning ${entity.plural} with a sourceId...`);
134
+ let ids: string[];
135
+ try {
136
+ ids = await collectIds(client, entity, { sourceId: { is: "NOT_NULL" } });
137
+ } catch (error) {
138
+ // opportunityProduct may not exist yet on this workspace.
139
+ report[entity.plural] = {
140
+ skipped:
141
+ error instanceof Error ? error.message.slice(0, 120) : "unknown",
142
+ };
143
+ continue;
144
+ }
145
+ if (!apply) {
146
+ report[entity.plural] = { wouldDestroy: ids.length };
147
+ continue;
148
+ }
149
+ const result = await destroyIds(client, entity, ids);
150
+ report[entity.plural] = result;
151
+ log(`destroyed ${result.destroyed} ${entity.plural}`);
152
+ }
153
+
154
+ // Workflow-created domain companies: no sourceId, createdBy.source WORKFLOW.
155
+ log("scanning workflow-created companies (no sourceId)...");
156
+ const workflowCompanies = await collectIds(
157
+ client,
158
+ COMPANY,
159
+ { sourceId: { is: "NULL" }, createdBy: { source: { eq: "WORKFLOW" } } },
160
+ "name",
161
+ );
162
+ if (!apply) {
163
+ report.workflowCompanies = { wouldDestroy: workflowCompanies.length };
164
+ } else {
165
+ const result = await destroyIds(client, COMPANY, workflowCompanies);
166
+ report.workflowCompanies = result;
167
+ log(`destroyed ${result.destroyed} workflow-created companies`);
168
+ }
169
+
170
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
171
+ }
172
+
173
+ main().catch((error) => {
174
+ process.stderr.write(
175
+ `${error instanceof Error ? error.message : String(error)}\n`,
176
+ );
177
+ process.exit(1);
178
+ });
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * One-off: turn opportunity products into a catalog + line-item model, and drop
4
+ * the legacy opportunity fields the first import left behind.
5
+ *
6
+ * Before: each line carried a free-text `product` holding one of 19 spellings
7
+ * of TEI's seven product lines ("MOBIL", "Mobil", "MOBIL - CVL", "GWO - PVL").
8
+ * The opportunity itself also carried leftover `product` and `quantity` fields
9
+ * that nothing writes any more.
10
+ *
11
+ * After: a `Product` catalog object holds exactly the seven lines LastMile
12
+ * offers (Ancillary, DEF, Fuel, Golden West, Hotsy, Mighty, Mobil), and each
13
+ * opportunity product line points at one. The Opportunity detail shows a
14
+ * Products section whose rows read Product / Quantity / Amount.
15
+ *
16
+ * Phases, in order, because each depends on the last:
17
+ * 1. DROP legacy `opportunity.product` and `opportunity.quantity`
18
+ * 2. DROP the free-text `opportunityProduct.product` (a RELATION cannot
19
+ * be created while a field of that name exists)
20
+ * 3. CREATE the `product` object, its sourceId, and seed the seven records
21
+ * 4. RELATE `opportunityProduct.product` -> `product` (MANY_TO_ONE)
22
+ * 5. VERIFY the relation is queryable before anything writes to it
23
+ *
24
+ * Then run `migrate-lastmile.ts --apply`: it rewrites every line with its
25
+ * catalog product, quantity, and amount from LastMile. Nothing is derived from
26
+ * the fields this script drops.
27
+ *
28
+ * Usage:
29
+ * npx tsx scripts/restructure-products.ts # dry-run: show the plan
30
+ * npx tsx scripts/restructure-products.ts --apply
31
+ */
32
+
33
+ import process from "node:process";
34
+
35
+ import {
36
+ emptyCounters,
37
+ upsertRecords,
38
+ type EntityShape,
39
+ } from "./lib/load-records";
40
+ import { mapProduct, PRODUCT_CATALOG } from "./lib/mappers";
41
+ import {
42
+ fetchObjectMetadata,
43
+ LEGACY_OPPORTUNITY_FIELDS,
44
+ PRODUCT_OBJECT,
45
+ } from "./lib/schema-ensure";
46
+ import { TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
47
+
48
+ const PRODUCT: EntityShape = {
49
+ singular: "product",
50
+ plural: "products",
51
+ capSingular: "Product",
52
+ capPlural: "Products",
53
+ };
54
+
55
+ function requireEnv(name: string): string {
56
+ const value = process.env[name];
57
+ if (!value) throw new Error(`Missing required environment variable: ${name}`);
58
+ return value;
59
+ }
60
+
61
+ function log(message: string): void {
62
+ process.stderr.write(`${message}\n`);
63
+ }
64
+
65
+ async function deleteField(
66
+ client: TwentyClient,
67
+ fieldId: string,
68
+ ): Promise<void> {
69
+ // Twenty requires a field be deactivated before it can be deleted.
70
+ await client.requestOnce(
71
+ "/metadata",
72
+ `mutation RestructureDeactivate($input: UpdateOneFieldMetadataInput!) {
73
+ updateOneField(input: $input) { id }
74
+ }`,
75
+ { input: { id: fieldId, update: { isActive: false } } },
76
+ );
77
+ await client.requestOnce(
78
+ "/metadata",
79
+ `mutation RestructureDeleteField($input: DeleteOneFieldInput!) {
80
+ deleteOneField(input: $input) { id }
81
+ }`,
82
+ { input: { id: fieldId } },
83
+ );
84
+ }
85
+
86
+ /**
87
+ * A newly created object or field is not usable the instant metadata accepts
88
+ * it — the same lag that silently coerced 1,510 stage values, and that made the
89
+ * first run of this script fail all 7 catalog inserts. Probe until it answers.
90
+ */
91
+ async function waitForQuery(
92
+ client: TwentyClient,
93
+ query: string,
94
+ what: string,
95
+ attempts = 15,
96
+ ): Promise<void> {
97
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
98
+ try {
99
+ await client.requestOnce("/graphql", query);
100
+ return;
101
+ } catch {
102
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
103
+ }
104
+ }
105
+ throw new Error(`${what} never became queryable; refusing to continue.`);
106
+ }
107
+
108
+ /** A relation is not usable the instant metadata accepts it. Probe until it answers. */
109
+ async function waitForRelation(
110
+ client: TwentyClient,
111
+ attempts = 15,
112
+ ): Promise<void> {
113
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
114
+ try {
115
+ await client.requestWithRetry(
116
+ "/graphql",
117
+ `query RestructureProbe {
118
+ opportunityProducts(first: 1) { edges { node { id product { id } } } }
119
+ }`,
120
+ );
121
+ return;
122
+ } catch {
123
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
124
+ }
125
+ }
126
+ throw new Error(
127
+ "opportunityProduct.product relation never became queryable; refusing to continue.",
128
+ );
129
+ }
130
+
131
+ async function main(): Promise<void> {
132
+ const apply = process.argv.slice(2).includes("--apply");
133
+ const client = new TwentyClient({
134
+ baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
135
+ authToken: requireEnv("TWENTY_API_KEY"),
136
+ });
137
+
138
+ let objects = await fetchObjectMetadata(client);
139
+ const report: Record<string, unknown> = { mode: apply ? "apply" : "dry-run" };
140
+
141
+ // --- 1 + 2: drop legacy and free-text fields ----------------------------
142
+ const opportunity = objects.get("opportunity");
143
+ const line = objects.get("opportunityProduct");
144
+ if (!opportunity) throw new Error('Twenty object "opportunity" not found.');
145
+
146
+ const toDrop: Array<{ label: string; id: string }> = [];
147
+ for (const name of LEGACY_OPPORTUNITY_FIELDS) {
148
+ const field = opportunity.fields.get(name);
149
+ if (field) toDrop.push({ label: `opportunity.${name}`, id: field.id });
150
+ }
151
+ const existingLineProduct = line?.fields.get("product");
152
+ if (existingLineProduct && existingLineProduct.type !== "RELATION") {
153
+ toDrop.push({
154
+ label: "opportunityProduct.product (TEXT)",
155
+ id: existingLineProduct.id,
156
+ });
157
+ }
158
+ report.dropFields = toDrop.map((field) => field.label);
159
+
160
+ if (apply) {
161
+ for (const field of toDrop) {
162
+ log(`dropping ${field.label}...`);
163
+ await deleteField(client, field.id);
164
+ }
165
+ if (toDrop.length > 0) objects = await fetchObjectMetadata(client);
166
+ }
167
+
168
+ // --- 3: create the catalog object and seed it ---------------------------
169
+ const productExists = objects.has(PRODUCT_OBJECT.nameSingular);
170
+ report.createProductObject = !productExists;
171
+ report.catalog = [...PRODUCT_CATALOG];
172
+
173
+ if (apply && !productExists) {
174
+ log("creating the Product catalog object...");
175
+ await client.requestOnce(
176
+ "/metadata",
177
+ `mutation RestructureCreateObject($input: CreateOneObjectInput!) {
178
+ createOneObject(input: $input) { id }
179
+ }`,
180
+ {
181
+ input: {
182
+ object: {
183
+ nameSingular: PRODUCT_OBJECT.nameSingular,
184
+ namePlural: PRODUCT_OBJECT.namePlural,
185
+ labelSingular: PRODUCT_OBJECT.labelSingular,
186
+ labelPlural: PRODUCT_OBJECT.labelPlural,
187
+ icon: PRODUCT_OBJECT.icon,
188
+ },
189
+ },
190
+ },
191
+ );
192
+ objects = await fetchObjectMetadata(client);
193
+ }
194
+
195
+ if (apply) {
196
+ const product = objects.get(PRODUCT_OBJECT.nameSingular);
197
+ if (!product) throw new Error("product object missing after creation.");
198
+ // Both fields: the upsert writes sourceId to find the record and sourceHash
199
+ // to decide whether it changed.
200
+ const productFields: Array<{
201
+ name: string;
202
+ label: string;
203
+ unique: boolean;
204
+ }> = [
205
+ { name: "sourceId", label: "Source ID", unique: true },
206
+ { name: "sourceHash", label: "Source Hash", unique: false },
207
+ ];
208
+ let createdAny = false;
209
+ for (const field of productFields) {
210
+ if (product.fields.has(field.name)) continue;
211
+ log(`creating product.${field.name}...`);
212
+ await client.requestOnce(
213
+ "/metadata",
214
+ `mutation RestructureCreateField($input: CreateOneFieldMetadataInput!) {
215
+ createOneField(input: $input) { id }
216
+ }`,
217
+ {
218
+ input: {
219
+ field: {
220
+ objectMetadataId: product.id,
221
+ name: field.name,
222
+ label: field.label,
223
+ type: "TEXT",
224
+ ...(field.unique ? { isUnique: true } : {}),
225
+ },
226
+ },
227
+ },
228
+ );
229
+ createdAny = true;
230
+ }
231
+ if (createdAny) objects = await fetchObjectMetadata(client);
232
+
233
+ log("waiting for the product object and sourceId to go live...");
234
+ await waitForQuery(
235
+ client,
236
+ `query RestructureProductProbe {
237
+ products(filter: { sourceId: { is: "NOT_NULL" } }, first: 1) {
238
+ edges { node { id sourceHash } }
239
+ }
240
+ }`,
241
+ "product.sourceId / product.sourceHash",
242
+ );
243
+
244
+ log(`seeding ${PRODUCT_CATALOG.length} catalog products...`);
245
+ const counters = emptyCounters();
246
+ await upsertRecords({
247
+ client,
248
+ entity: PRODUCT,
249
+ mapped: PRODUCT_CATALOG.map(mapProduct),
250
+ dryRun: false,
251
+ counters,
252
+ });
253
+ report.catalogSeed = {
254
+ created: counters.created,
255
+ skipped: counters.skipped,
256
+ failed: counters.failed,
257
+ gaps: counters.gaps.slice(0, 5),
258
+ };
259
+ if (counters.failed > 0) {
260
+ throw new Error(
261
+ `Catalog seed failed for ${counters.failed} product(s): ${counters.gaps[0] ?? "unknown"}`,
262
+ );
263
+ }
264
+ }
265
+
266
+ // --- 4 + 5: relate the line to the catalog, then verify ------------------
267
+ const lineAfter = objects.get("opportunityProduct");
268
+ const hasRelation = lineAfter?.fields.get("product")?.type === "RELATION";
269
+ report.createRelation = !hasRelation;
270
+
271
+ if (apply && !hasRelation) {
272
+ const product = objects.get(PRODUCT_OBJECT.nameSingular);
273
+ if (!product || !lineAfter)
274
+ throw new Error("objects missing for relation.");
275
+ log("relating opportunityProduct.product -> product...");
276
+ await client.requestOnce(
277
+ "/metadata",
278
+ `mutation RestructureRelate($input: CreateOneFieldMetadataInput!) {
279
+ createOneField(input: $input) { id }
280
+ }`,
281
+ {
282
+ input: {
283
+ field: {
284
+ objectMetadataId: lineAfter.id,
285
+ name: "product",
286
+ label: "Product",
287
+ type: "RELATION",
288
+ relationCreationPayload: {
289
+ targetObjectMetadataId: product.id,
290
+ targetFieldLabel: PRODUCT_OBJECT.targetFieldLabel,
291
+ targetFieldIcon: PRODUCT_OBJECT.icon,
292
+ type: "MANY_TO_ONE",
293
+ },
294
+ },
295
+ },
296
+ },
297
+ );
298
+ log("waiting for the relation to go live...");
299
+ await waitForRelation(client);
300
+ }
301
+
302
+ report.next =
303
+ "run migrate-lastmile.ts --apply to point every line at its catalog product";
304
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
305
+ }
306
+
307
+ main().catch((error) => {
308
+ process.stderr.write(
309
+ `${error instanceof Error ? error.message : String(error)}\n`,
310
+ );
311
+ process.exit(1);
312
+ });