@useairfoil/producer-linear 0.0.1 → 0.0.2

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 (50) hide show
  1. package/README.md +71 -0
  2. package/dist/index.d.mts +4027 -0
  3. package/dist/index.mjs +3 -0
  4. package/dist/main.d.mts +1 -0
  5. package/dist/main.mjs +1303 -0
  6. package/dist/main.mjs.map +1 -0
  7. package/dist/manifest.d.mts +114 -0
  8. package/dist/manifest.mjs +161 -0
  9. package/dist/manifest.mjs.map +1 -0
  10. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  11. package/dist/src-Ci3_ltan.mjs +1691 -0
  12. package/dist/src-Ci3_ltan.mjs.map +1 -0
  13. package/package.json +45 -7
  14. package/src/client/client.ts +200 -0
  15. package/src/connector.ts +151 -0
  16. package/src/create-table.ts +5 -0
  17. package/src/demo/index.ts +137 -0
  18. package/src/demo/initiatives.ts +116 -0
  19. package/src/demo/issues.ts +320 -0
  20. package/src/demo/projects.ts +215 -0
  21. package/src/demo/setup.ts +151 -0
  22. package/src/demo/shared.ts +122 -0
  23. package/src/demo/teams.ts +173 -0
  24. package/src/index.ts +83 -0
  25. package/src/main.ts +22 -0
  26. package/src/manifest.ts +58 -0
  27. package/src/resources/comments.ts +32 -0
  28. package/src/resources/customers.ts +105 -0
  29. package/src/resources/documents.ts +31 -0
  30. package/src/resources/initiatives.ts +136 -0
  31. package/src/resources/issue-history.ts +119 -0
  32. package/src/resources/issues.ts +131 -0
  33. package/src/resources/pages.ts +264 -0
  34. package/src/resources/projects.ts +161 -0
  35. package/src/resources/releases.ts +114 -0
  36. package/src/resources/teams.ts +140 -0
  37. package/src/sandbox.ts +35 -0
  38. package/src/schemas/comments.ts +32 -0
  39. package/src/schemas/customers.ts +80 -0
  40. package/src/schemas/documents.ts +32 -0
  41. package/src/schemas/initiatives.ts +119 -0
  42. package/src/schemas/issues.ts +180 -0
  43. package/src/schemas/projects.ts +154 -0
  44. package/src/schemas/releases.ts +119 -0
  45. package/src/schemas/shared.ts +66 -0
  46. package/src/schemas/teams.ts +144 -0
  47. package/src/start.ts +47 -0
  48. package/src/tables.ts +83 -0
  49. package/src/webhook/route.ts +50 -0
  50. package/src/webhook/signature.ts +28 -0
@@ -0,0 +1,320 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import type { Comment } from "../schemas/comments";
4
+
5
+ import {
6
+ clock,
7
+ customerCount,
8
+ customerId,
9
+ cycleId,
10
+ cyclesBack,
11
+ cycleWeeks,
12
+ dateOnly,
13
+ day,
14
+ hour,
15
+ issueId,
16
+ issueLabels,
17
+ labelId,
18
+ milestoneId,
19
+ months,
20
+ priorities,
21
+ projectId,
22
+ projects,
23
+ releaseId,
24
+ type Rows,
25
+ stamp,
26
+ stateId,
27
+ states,
28
+ teamId,
29
+ teams,
30
+ userCount,
31
+ userId,
32
+ uuid,
33
+ verbs,
34
+ workspace,
35
+ } from "./shared";
36
+
37
+ /** One group is one issue, with its history, comments, and links. */
38
+ export const issueRows = (index: number, context: Demo.DemoContext): Rows => {
39
+ const { faker, now } = context;
40
+ const { at, later } = clock(context);
41
+
42
+ const rows: Rows = {
43
+ issues: [],
44
+ issue_history: [],
45
+ comments: [],
46
+ issue_relations: [],
47
+ attachments: [],
48
+ customer_needs: [],
49
+ issue_to_releases: [],
50
+ };
51
+
52
+ const t = index % teams.length;
53
+ const team = teams[t] ?? teams[0];
54
+ const number = Math.floor(index / teams.length) + 1;
55
+ const identifier = `${team.key}-${number}`;
56
+ const id = issueId(index);
57
+ const title = `${faker.helpers.arrayElement(verbs)} ${faker.hacker.adjective()} ${faker.hacker.noun()}`;
58
+ const slug = title.toLowerCase().replaceAll(" ", "-");
59
+ const url = `${workspace}/issue/${identifier}/${slug}`;
60
+
61
+ const creator = faker.number.int({ max: userCount - 1 });
62
+
63
+ const assignee = faker.helpers.maybe(() => faker.number.int({ max: userCount - 1 }), {
64
+ probability: 0.85,
65
+ });
66
+
67
+ const created = at(now.getTime() - faker.number.int({ min: day, max: 360 * day }));
68
+
69
+ const outcome = faker.helpers.weightedArrayElement([
70
+ { weight: 55, value: "Done" },
71
+ { weight: 8, value: "Canceled" },
72
+ { weight: 12, value: "In Progress" },
73
+ { weight: 5, value: "In Review" },
74
+ { weight: 10, value: "Todo" },
75
+ { weight: 10, value: "Backlog" },
76
+ ] as const);
77
+
78
+ const started =
79
+ outcome === "Done" || outcome === "In Progress" || outcome === "In Review"
80
+ ? later(created, hour, 20 * day)
81
+ : null;
82
+
83
+ const completed = outcome === "Done" && started ? later(started, hour, 14 * day) : null;
84
+ const canceled = outcome === "Canceled" ? later(created, day, 30 * day) : null;
85
+ const triaged = team.triage ? later(created, 60_000, day) : null;
86
+
87
+ const project = faker.helpers.maybe(() => faker.number.int({ max: projects.length - 1 }), {
88
+ probability: 0.6,
89
+ });
90
+
91
+ const cycleNumber =
92
+ team.cycles && started
93
+ ? Math.max(
94
+ 0,
95
+ Math.min(
96
+ cyclesBack,
97
+ cyclesBack - Math.ceil((now.getTime() - started.getTime()) / (cycleWeeks * 7 * day)),
98
+ ),
99
+ )
100
+ : null;
101
+
102
+ const label = faker.number.int({ max: issueLabels.length - 1 });
103
+
104
+ const priority = faker.helpers.weightedArrayElement([
105
+ { weight: 10, value: 0 },
106
+ { weight: 5, value: 1 },
107
+ { weight: 25, value: 2 },
108
+ { weight: 40, value: 3 },
109
+ { weight: 20, value: 4 },
110
+ ]);
111
+
112
+ const estimate = team.key === "ENG" ? faker.helpers.arrayElement([1, 2, 3, 5, 8]) : null;
113
+ const stateIndex = states.findIndex((state) => state.name === outcome);
114
+
115
+ // History: created, assigned, then each state change.
116
+ const changes: Array<{ readonly at: Date; readonly from: number | null; readonly to: number }> = [
117
+ { at: created, from: null, to: team.triage ? 0 : 1 },
118
+ ];
119
+
120
+ if (triaged) changes.push({ at: triaged, from: 0, to: 1 });
121
+ if (started) changes.push({ at: started, from: 1, to: 3 });
122
+
123
+ if (outcome === "In Review" && started)
124
+ changes.push({ at: later(started, day, 5 * day), from: 3, to: 4 });
125
+
126
+ if (completed) changes.push({ at: completed, from: 3, to: 5 });
127
+ if (canceled) changes.push({ at: canceled, from: 1, to: 6 });
128
+ if (outcome === "Todo") changes.push({ at: later(created, 60_000, 3 * day), from: 1, to: 2 });
129
+
130
+ changes.forEach((change, h) => {
131
+ rows.issue_history.push({
132
+ id: uuid(30, index, h),
133
+ ...stamp(change.at, change.at),
134
+ issue_id: id,
135
+ actor_id: userId(h === 0 ? creator : (assignee ?? creator)),
136
+ from_state_id: change.from === null ? null : stateId(t, change.from),
137
+ to_state_id: stateId(t, change.to),
138
+ from_assignee_id: null,
139
+ to_assignee_id: h === 0 && assignee !== undefined ? userId(assignee) : null,
140
+ from_cycle_id: null,
141
+ to_cycle_id: change.to === 3 && cycleNumber !== null ? cycleId(t, cycleNumber) : null,
142
+ from_project_id: null,
143
+ to_project_id: h === 0 && project !== undefined ? projectId(project) : null,
144
+ from_parent_id: null,
145
+ to_parent_id: null,
146
+ from_team_id: null,
147
+ to_team_id: null,
148
+ from_priority: null,
149
+ to_priority: h === 0 ? priority : null,
150
+ from_estimate: null,
151
+ to_estimate: h === 0 ? estimate : null,
152
+ from_due_date: null,
153
+ to_due_date: null,
154
+ from_title: null,
155
+ to_title: null,
156
+ added_label_ids: h === 0 ? [labelId(label)] : null,
157
+ removed_label_ids: null,
158
+ attachment_id: null,
159
+ customer_need_id: null,
160
+ to_converted_project_id: null,
161
+ updated_description: null,
162
+ archived: null,
163
+ trashed: null,
164
+ auto_archived: null,
165
+ auto_closed: null,
166
+ });
167
+ });
168
+
169
+ const comments = Array.from({ length: faker.number.int({ max: 3 }) }, (_, c) => {
170
+ const posted = later(created, 60_000, 10 * day);
171
+
172
+ return {
173
+ id: uuid(31, index, c),
174
+ ...stamp(posted, posted),
175
+ _deleted: false,
176
+ body: faker.helpers.arrayElement([
177
+ "I can reproduce this on staging.",
178
+ "Picking this up now.",
179
+ "Added a fix behind a feature flag.",
180
+ "Can we pair on this tomorrow?",
181
+ "Customer confirmed it works now.",
182
+ ]),
183
+ user_id: userId(faker.number.int({ max: userCount - 1 })),
184
+ external_user_id: null,
185
+ issue_id: id,
186
+ project_id: null,
187
+ project_update_id: null,
188
+ initiative_id: null,
189
+ initiative_update_id: null,
190
+ document_content_id: null,
191
+ parent_id: null,
192
+ quoted_text: null,
193
+ edited_at: null,
194
+ resolved_at: null,
195
+ resolving_user_id: null,
196
+ url: `${url}#comment-${uuid(31, index, c).slice(-8)}`,
197
+ } satisfies Comment;
198
+ });
199
+
200
+ rows.comments.push(...comments);
201
+
202
+ if (index >= teams.length && faker.datatype.boolean({ probability: 0.1 })) {
203
+ rows.issue_relations.push({
204
+ id: uuid(32, index),
205
+ ...stamp(created, created),
206
+ issue_id: id,
207
+ // The issue before this one in the same team.
208
+ related_issue_id: issueId(index - teams.length),
209
+ type: faker.helpers.arrayElement(["blocks", "related", "duplicate"]),
210
+ });
211
+ }
212
+
213
+ if (team.key === "ENG" && started) {
214
+ const prNumber = 1000 + index;
215
+
216
+ rows.attachments.push({
217
+ id: uuid(33, index),
218
+ ...stamp(started, completed ?? started),
219
+ _deleted: false,
220
+ issue_id: id,
221
+ creator_id: assignee === undefined ? null : userId(assignee),
222
+ external_user_creator_id: null,
223
+ title: `${title} #${prNumber}`,
224
+ subtitle: completed ? "Merged" : "Open",
225
+ url: `https://github.com/acme-labs/web/pull/${prNumber}`,
226
+ source_type: "github",
227
+ });
228
+ }
229
+
230
+ if (faker.datatype.boolean({ probability: 0.15 })) {
231
+ const asked = later(created, 0, 5 * day);
232
+
233
+ rows.customer_needs.push({
234
+ id: uuid(34, index),
235
+ ...stamp(asked, asked),
236
+ _deleted: false,
237
+ customer_id: customerId(faker.number.int({ max: customerCount - 1 })),
238
+ issue_id: id,
239
+ project_id: null,
240
+ original_issue_id: id,
241
+ comment_id: null,
242
+ attachment_id: null,
243
+ creator_id: userId(creator),
244
+ body: "Their team asked for this on the last call.",
245
+ priority: faker.helpers.arrayElement([0, 0, 1]),
246
+ url: null,
247
+ });
248
+ }
249
+
250
+ // Engineering work ships in the monthly release after it's done.
251
+ if (team.key === "ENG" && completed) {
252
+ const month = Math.floor((now.getTime() - completed.getTime()) / (30 * day));
253
+
254
+ if (month < months) {
255
+ rows.issue_to_releases.push({
256
+ id: uuid(35, index),
257
+ ...stamp(completed, completed),
258
+ issue_id: id,
259
+ release_id: releaseId(month),
260
+ });
261
+ }
262
+ }
263
+
264
+ const updated = new Date(
265
+ Math.max(
266
+ ...changes.map((change) => change.at.getTime()),
267
+ ...comments.map((row) => row.created_at.getTime()),
268
+ ),
269
+ );
270
+
271
+ rows.issues.push({
272
+ id,
273
+ ...stamp(created, updated),
274
+ _deleted: false,
275
+ identifier,
276
+ number,
277
+ title,
278
+ description: `${faker.hacker.phrase()}\n\n**Steps**\n\n1. Open the page\n2. Try the action\n3. See the error`,
279
+ url,
280
+ branch_name: `${identifier.toLowerCase()}-${slug}`,
281
+ priority,
282
+ priority_label: priorities[priority] ?? "No priority",
283
+ estimate,
284
+ due_date: priority === 1 ? dateOnly(new Date(created.getTime() + 7 * day)) : null,
285
+ creator_id: userId(creator),
286
+ assignee_id: assignee === undefined ? null : userId(assignee),
287
+ delegate_id: null,
288
+ snoozed_by_id: null,
289
+ state_id: stateId(t, stateIndex),
290
+ team_id: teamId(t),
291
+ project_id: project === undefined ? null : projectId(project),
292
+ project_milestone_id:
293
+ project === undefined ? null : milestoneId(project, faker.number.int({ max: 2 })),
294
+ cycle_id: cycleNumber === null ? null : cycleId(t, cycleNumber),
295
+ parent_id: null,
296
+ source_comment_id: null,
297
+ external_user_creator_id: null,
298
+ label_ids: [labelId(label)],
299
+ previous_identifiers: [],
300
+ started_at: started,
301
+ completed_at: completed,
302
+ canceled_at: canceled,
303
+ started_triage_at: team.triage ? created : null,
304
+ triaged_at: triaged,
305
+ auto_closed_at: null,
306
+ auto_archived_at: null,
307
+ added_to_cycle_at: cycleNumber === null ? null : started,
308
+ added_to_project_at: project === undefined ? null : created,
309
+ added_to_team_at: created,
310
+ snoozed_until_at: null,
311
+ sla_type: null,
312
+ sla_started_at: null,
313
+ sla_breaches_at: null,
314
+ customer_ticket_count: BigInt(rows.customer_needs.length),
315
+ integration_source_type: null,
316
+ trashed: false,
317
+ });
318
+
319
+ return rows;
320
+ };
@@ -0,0 +1,215 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import type { Document } from "../schemas/documents";
4
+ import type {
5
+ Project,
6
+ ProjectLabel,
7
+ ProjectMilestone,
8
+ ProjectRelation,
9
+ ProjectStatus,
10
+ ProjectUpdate,
11
+ } from "../schemas/projects";
12
+
13
+ import {
14
+ dateOnly,
15
+ day,
16
+ milestoneId,
17
+ priorities,
18
+ projectId,
19
+ projects,
20
+ projectStatuses,
21
+ stamp,
22
+ teamId,
23
+ teams,
24
+ userCount,
25
+ userId,
26
+ uuid,
27
+ workspace,
28
+ } from "./shared";
29
+
30
+ /** Projects with their milestones, updates, specs, and relations. */
31
+ export const projectTables = ({ faker, now }: Demo.DemoContext, founded: Date) => {
32
+ const set = stamp(founded, founded);
33
+
34
+ const statusRows: Array<ProjectStatus> = projectStatuses.map((status, s) => ({
35
+ id: uuid(15, s),
36
+ ...set,
37
+ name: status.name,
38
+ type: status.type,
39
+ color: status.color,
40
+ description: null,
41
+ position: s,
42
+ }));
43
+
44
+ const projectLabels: Array<ProjectLabel> = ["Q1", "Q2", "Customer request", "Tech debt"].map(
45
+ (name, l) => ({
46
+ id: uuid(16, l),
47
+ ...set,
48
+ _deleted: false,
49
+ name,
50
+ description: null,
51
+ color: faker.color.rgb({ casing: "upper" }),
52
+ is_group: false,
53
+ parent_id: null,
54
+ creator_id: userId(1),
55
+ retired_at: null,
56
+ }),
57
+ );
58
+
59
+ const projectRows: Array<Project> = [];
60
+ const milestones: Array<ProjectMilestone> = [];
61
+ const updates: Array<ProjectUpdate> = [];
62
+ const documents: Array<Document> = [];
63
+
64
+ projects.forEach((name, p) => {
65
+ const started = new Date(now.getTime() - (360 - p * 40) * day);
66
+ const target = new Date(started.getTime() + 120 * day);
67
+ const completed = target < now;
68
+ const status = completed ? 4 : p % 3 === 2 ? 1 : 2;
69
+ const lead = (p * 3) % userCount;
70
+
71
+ const health = completed
72
+ ? null
73
+ : faker.helpers.arrayElement(["onTrack", "onTrack", "atRisk", "offTrack"]);
74
+
75
+ const updated = completed ? target : now;
76
+ const slug = name.toLowerCase().replaceAll(" ", "-");
77
+
78
+ projectRows.push({
79
+ id: projectId(p),
80
+ ...stamp(started, updated),
81
+ _deleted: false,
82
+ name,
83
+ description: `${name} for the ${teams[p % teams.length]?.name ?? "Engineering"} team`,
84
+ content: `## Goal\n\nShip ${name.toLowerCase()} by ${dateOnly(target)}.`,
85
+ url: `${workspace}/project/${slug}-${p.toString(16).padStart(12, "0")}`,
86
+ icon: null,
87
+ color: faker.color.rgb({ casing: "upper" }),
88
+ status_id: uuid(15, status),
89
+ priority: BigInt(1 + (p % 4)),
90
+ priority_label: priorities[1 + (p % 4)] ?? "Medium",
91
+ health,
92
+ health_updated_at: health ? new Date(now.getTime() - 3 * day) : null,
93
+ progress: completed ? 1 : faker.number.float({ min: 0.1, max: 0.8, fractionDigits: 2 }),
94
+ scope: faker.number.int({ min: 20, max: 80 }),
95
+ lead_id: userId(lead),
96
+ creator_id: userId(lead),
97
+ converted_from_issue_id: null,
98
+ start_date: dateOnly(started),
99
+ target_date: dateOnly(target),
100
+ started_at: status === 1 ? null : started,
101
+ completed_at: completed ? target : null,
102
+ canceled_at: null,
103
+ auto_archived_at: null,
104
+ label_ids: [uuid(16, p % 4)],
105
+ team_ids: [teamId(p % teams.length)],
106
+ member_ids: [userId(lead), userId((lead + 1) % userCount)],
107
+ trashed: false,
108
+ });
109
+
110
+ ["Alpha", "Beta", "GA"].forEach((stage, m) => {
111
+ const milestoneTarget = new Date(started.getTime() + (m + 1) * 40 * day);
112
+
113
+ milestones.push({
114
+ id: milestoneId(p, m),
115
+ ...stamp(started, started),
116
+ name: stage,
117
+ description: null,
118
+ project_id: projectId(p),
119
+ target_date: dateOnly(milestoneTarget),
120
+ status:
121
+ milestoneTarget <= now
122
+ ? "done"
123
+ : m === 0 || milestoneTarget.getTime() - now.getTime() < 40 * day
124
+ ? "next"
125
+ : "unstarted",
126
+ progress: milestoneTarget <= now ? 1 : 0,
127
+ sort_order: m,
128
+ });
129
+ });
130
+
131
+ // A weekly update while the project runs.
132
+ for (
133
+ let week = started.getTime() + 7 * day;
134
+ week <= Math.min(updated.getTime(), now.getTime());
135
+ week += 7 * day
136
+ ) {
137
+ const posted = new Date(week);
138
+
139
+ updates.push({
140
+ id: uuid(17, p, updates.length),
141
+ ...stamp(posted, posted),
142
+ _deleted: false,
143
+ project_id: projectId(p),
144
+ user_id: userId(lead),
145
+ body: faker.helpers.arrayElement([
146
+ "Good week. The main flows are done and we're testing edge cases.",
147
+ "We hit a snag with the migration. It may slip a few days.",
148
+ "On track. Design review is done and we start the rollout next week.",
149
+ ]),
150
+ health: faker.helpers.arrayElement(["onTrack", "onTrack", "atRisk"]),
151
+ url: `${workspace}/project/${slug}/updates#${updates.length}`,
152
+ edited_at: null,
153
+ comment_count: 0n,
154
+ });
155
+ }
156
+
157
+ documents.push({
158
+ id: uuid(18, p),
159
+ ...stamp(started, started),
160
+ _deleted: false,
161
+ title: `${name} spec`,
162
+ content: `# ${name}\n\n## Problem\n\nCustomers asked for this.\n\n## Plan\n\nShip it in three milestones.`,
163
+ url: `${workspace}/document/${slug}-spec-${p.toString(16).padStart(12, "0")}`,
164
+ icon: null,
165
+ color: null,
166
+ creator_id: userId(lead),
167
+ updated_by_id: userId(lead),
168
+ project_id: projectId(p),
169
+ initiative_id: null,
170
+ issue_id: null,
171
+ cycle_id: null,
172
+ team_id: null,
173
+ parent_document_id: null,
174
+ hidden_at: null,
175
+ trashed: false,
176
+ });
177
+ });
178
+
179
+ const projectRelations: Array<ProjectRelation> = [
180
+ {
181
+ id: uuid(19, 0),
182
+ ...set,
183
+ type: "dependency",
184
+ project_id: projectId(4),
185
+ project_milestone_id: null,
186
+ anchor_type: "start",
187
+ related_project_id: projectId(1),
188
+ related_project_milestone_id: null,
189
+ related_anchor_type: "end",
190
+ user_id: userId(0),
191
+ },
192
+ {
193
+ id: uuid(19, 1),
194
+ ...set,
195
+ type: "dependency",
196
+ project_id: projectId(6),
197
+ project_milestone_id: null,
198
+ anchor_type: "start",
199
+ related_project_id: projectId(3),
200
+ related_project_milestone_id: null,
201
+ related_anchor_type: "end",
202
+ user_id: userId(1),
203
+ },
204
+ ];
205
+
206
+ return {
207
+ project_statuses: statusRows,
208
+ project_labels: projectLabels,
209
+ projects: projectRows,
210
+ project_milestones: milestones,
211
+ project_updates: updates,
212
+ project_relations: projectRelations,
213
+ documents,
214
+ };
215
+ };
@@ -0,0 +1,151 @@
1
+ import type { Demo } from "@useairfoil/connector-kit";
2
+
3
+ import type { Customer, CustomerStatus, CustomerTier } from "../schemas/customers";
4
+ import type { Release, ReleaseNote, ReleasePipeline, ReleaseStage } from "../schemas/releases";
5
+
6
+ import { initiativeTables } from "./initiatives";
7
+ import { projectTables } from "./projects";
8
+ import {
9
+ customerCount,
10
+ customerId,
11
+ dateOnly,
12
+ day,
13
+ months,
14
+ pipelineId,
15
+ releaseId,
16
+ stamp,
17
+ userCount,
18
+ userId,
19
+ uuid,
20
+ workspace,
21
+ } from "./shared";
22
+ import { teamTables } from "./teams";
23
+
24
+ /** The workspace, written once before any issue. */
25
+ export const setupRows = (context: Demo.DemoContext) => {
26
+ const { faker, now } = context;
27
+ const founded = new Date(now.getTime() - 900 * day);
28
+ const set = stamp(founded, founded);
29
+
30
+ const teamSetup = teamTables(context, founded);
31
+ const projectSetup = projectTables(context, founded);
32
+ const initiativeSetup = initiativeTables(context, founded);
33
+
34
+ const customerStatuses: Array<CustomerStatus> = ["Active", "Trial", "Churned"].map((name, s) => ({
35
+ id: uuid(24, s),
36
+ ...set,
37
+ name: name.toLowerCase(),
38
+ display_name: name,
39
+ description: null,
40
+ color: ["#4CB782", "#F2C94C", "#95A2B3"][s] ?? "#95A2B3",
41
+ position: s,
42
+ }));
43
+
44
+ const customerTiers: Array<CustomerTier> = ["Enterprise", "Growth", "Starter"].map((name, s) => ({
45
+ id: uuid(25, s),
46
+ ...set,
47
+ name: name.toLowerCase(),
48
+ display_name: name,
49
+ description: null,
50
+ color: ["#5E6AD2", "#4EA7FC", "#BEC2C8"][s] ?? "#BEC2C8",
51
+ position: s,
52
+ }));
53
+
54
+ const customers: Array<Customer> = Array.from({ length: customerCount }, (_, c) => {
55
+ const name = faker.company.name();
56
+ const domain = `${name.toLowerCase().replaceAll(/[^a-z]+/g, "")}.example.com`;
57
+ const tier = c % 3;
58
+
59
+ return {
60
+ id: customerId(c),
61
+ ...stamp(founded, now),
62
+ _deleted: false,
63
+ name,
64
+ domains: [domain],
65
+ external_ids: [],
66
+ logo_url: null,
67
+ owner_id: userId(c % userCount),
68
+ status_id: uuid(24, c === customerCount - 1 ? 2 : c % 5 === 4 ? 1 : 0),
69
+ tier_id: uuid(25, tier),
70
+ revenue: BigInt([250_000, 60_000, 12_000][tier] ?? 12_000),
71
+ size: [2_000, 300, 40][tier] ?? 40,
72
+ approximate_need_count: 0,
73
+ url: `${workspace}/customer/${name.toLowerCase().replaceAll(/[^a-z]+/g, "-")}`,
74
+ };
75
+ });
76
+
77
+ const pipeline: Array<ReleasePipeline> = [
78
+ {
79
+ id: pipelineId,
80
+ ...set,
81
+ name: "Web app",
82
+ type: "scheduled",
83
+ is_production: true,
84
+ url: `${workspace}/releases/web-app`,
85
+ include_path_patterns: ["apps/web/**"],
86
+ approximate_release_count: BigInt(months),
87
+ trashed: false,
88
+ },
89
+ ];
90
+
91
+ const stages: Array<ReleaseStage> = ["Planned", "In progress", "Released"].map((name, s) => ({
92
+ id: uuid(26, s),
93
+ ...set,
94
+ name,
95
+ type: ["planned", "started", "completed"][s] ?? "completed",
96
+ color: ["#BEC2C8", "#F2C94C", "#5E6AD2"][s] ?? "#5E6AD2",
97
+ position: s,
98
+ frozen: false,
99
+ pipeline_id: pipelineId,
100
+ }));
101
+
102
+ // One release a month. The newest one is still in progress.
103
+ const releases: Array<Release> = Array.from({ length: months }, (_, m) => {
104
+ const released = new Date(now.getTime() - m * 30 * day);
105
+ const current = m === 0;
106
+
107
+ return {
108
+ id: releaseId(m),
109
+ ...stamp(new Date(released.getTime() - 30 * day), current ? now : released),
110
+ name: `Release 2.${months - m}.0`,
111
+ description: null,
112
+ release_version: `2.${months - m}.0`,
113
+ url: `${workspace}/releases/web-app/2.${months - m}.0`,
114
+ commit_sha: current ? null : faker.git.commitSha(),
115
+ pipeline_id: pipelineId,
116
+ stage_id: uuid(26, current ? 1 : 2),
117
+ creator_id: userId(0),
118
+ start_date: dateOnly(new Date(released.getTime() - 30 * day)),
119
+ target_date: dateOnly(released),
120
+ started_at: new Date(released.getTime() - 30 * day),
121
+ completed_at: current ? null : released,
122
+ canceled_at: null,
123
+ trashed: false,
124
+ };
125
+ });
126
+
127
+ const releaseNotes: Array<ReleaseNote> = releases
128
+ .filter((release) => release.completed_at !== null)
129
+ .map((release, k) => ({
130
+ id: uuid(27, k),
131
+ ...stamp(release.completed_at ?? now, release.completed_at ?? now),
132
+ title: `What's new in ${release.release_version}`,
133
+ content: "Faster search, clearer billing pages, and many small fixes.",
134
+ url: `${release.url}/notes`,
135
+ pipeline_id: pipelineId,
136
+ release_ids: [release.id],
137
+ }));
138
+
139
+ return {
140
+ ...teamSetup,
141
+ ...projectSetup,
142
+ ...initiativeSetup,
143
+ customer_statuses: customerStatuses,
144
+ customer_tiers: customerTiers,
145
+ customers,
146
+ release_pipelines: pipeline,
147
+ release_stages: stages,
148
+ releases,
149
+ release_notes: releaseNotes,
150
+ };
151
+ };