@useairfoil/producer-linear 0.0.1 → 0.0.3

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 (51) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +71 -0
  3. package/dist/index.d.mts +4027 -0
  4. package/dist/index.mjs +3 -0
  5. package/dist/main.d.mts +1 -0
  6. package/dist/main.mjs +1305 -0
  7. package/dist/main.mjs.map +1 -0
  8. package/dist/manifest.d.mts +114 -0
  9. package/dist/manifest.mjs +161 -0
  10. package/dist/manifest.mjs.map +1 -0
  11. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  12. package/dist/src-BbzySPR7.mjs +1688 -0
  13. package/dist/src-BbzySPR7.mjs.map +1 -0
  14. package/package.json +46 -8
  15. package/src/client/client.ts +200 -0
  16. package/src/connector.ts +151 -0
  17. package/src/create-table.ts +10 -0
  18. package/src/demo/index.ts +137 -0
  19. package/src/demo/initiatives.ts +116 -0
  20. package/src/demo/issues.ts +320 -0
  21. package/src/demo/projects.ts +215 -0
  22. package/src/demo/setup.ts +151 -0
  23. package/src/demo/shared.ts +122 -0
  24. package/src/demo/teams.ts +173 -0
  25. package/src/index.ts +83 -0
  26. package/src/main.ts +28 -0
  27. package/src/manifest.ts +58 -0
  28. package/src/resources/comments.ts +32 -0
  29. package/src/resources/customers.ts +105 -0
  30. package/src/resources/documents.ts +31 -0
  31. package/src/resources/initiatives.ts +136 -0
  32. package/src/resources/issue-history.ts +119 -0
  33. package/src/resources/issues.ts +131 -0
  34. package/src/resources/pages.ts +264 -0
  35. package/src/resources/projects.ts +161 -0
  36. package/src/resources/releases.ts +114 -0
  37. package/src/resources/teams.ts +140 -0
  38. package/src/sandbox.ts +35 -0
  39. package/src/schemas/comments.ts +52 -0
  40. package/src/schemas/customers.ts +96 -0
  41. package/src/schemas/documents.ts +40 -0
  42. package/src/schemas/initiatives.ts +155 -0
  43. package/src/schemas/issues.ts +262 -0
  44. package/src/schemas/projects.ts +210 -0
  45. package/src/schemas/releases.ts +132 -0
  46. package/src/schemas/shared.ts +68 -0
  47. package/src/schemas/teams.ts +177 -0
  48. package/src/start.ts +47 -0
  49. package/src/tables.ts +83 -0
  50. package/src/webhook/route.ts +50 -0
  51. package/src/webhook/signature.ts +28 -0
@@ -0,0 +1,1688 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { LinearConfigDef, manifest } from "./manifest.mjs";
3
+ import { Connector, ConnectorError, Cursor, Fetch, Iceberg, Metrics, Resource, Telemetry, Webhook } from "@useairfoil/connector-kit";
4
+ import { Array, Config, Context, Data, DateTime, Duration, Effect, Layer, Option, Redacted, Result, Schedule, Schema, SchemaTransformation, Stream, String as String$1, Struct } from "effect";
5
+ import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/http";
6
+ import { createHmac, timingSafeEqual } from "node:crypto";
7
+ //#region src/client/client.ts
8
+ var client_exports = /* @__PURE__ */ __exportAll({
9
+ LinearClient: () => LinearClient,
10
+ layer: () => layer$1,
11
+ layerConfig: () => layerConfig$1,
12
+ make: () => make$1
13
+ });
14
+ const endpoint = "https://api.linear.app/graphql";
15
+ const maxRetries = 5;
16
+ const retryBaseDelay = Duration.seconds(1);
17
+ const requestTimeout = Duration.minutes(2);
18
+ const limitRetry = Schedule.min([Schedule.exponential("1 minute"), Schedule.spaced("10 minutes")]);
19
+ var LinearClient = class extends Context.Service()("@useairfoil/producer-linear/LinearClient") {};
20
+ const ResponseSchema = Schema.Struct({
21
+ data: Schema.optional(Schema.Unknown),
22
+ errors: Schema.optional(Schema.Array(Schema.Struct({
23
+ message: Schema.String,
24
+ extensions: Schema.optional(Schema.Struct({ code: Schema.optional(Schema.String) }))
25
+ })))
26
+ });
27
+ var RateLimited = class extends Data.TaggedError("RateLimited") {};
28
+ const isNotFound = (message) => message.startsWith("Entity not found");
29
+ const operationName = (document) => /^\s*query\s+(\w+)/.exec(document)?.[1] ?? "query";
30
+ const make$1 = Effect.fnUntraced(function* (config) {
31
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.mapRequest((request) => request.pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeader("Authorization", Redacted.value(config.apiKey)))), HttpClient.retryTransient({ schedule: Metrics.retrySchedule({
32
+ connector: manifest.name,
33
+ baseDelay: retryBaseDelay,
34
+ times: maxRetries
35
+ }) }));
36
+ const execute = (schema, document, variables = {}) => {
37
+ const operation = operationName(document);
38
+ return Effect.gen(function* () {
39
+ const response = yield* client.execute(HttpClientRequest.post(endpoint).pipe(HttpClientRequest.bodyJsonUnsafe({
40
+ query: document,
41
+ variables
42
+ }))).pipe(Effect.tapError((error) => Telemetry.annotateError("api_http", error)), Effect.mapError((error) => new ConnectorError({
43
+ message: `Linear API request failed for ${operation}`,
44
+ cause: error
45
+ })));
46
+ const body = yield* HttpClientResponse.schemaBodyJson(ResponseSchema)(response).pipe(Effect.tapError((error) => Telemetry.annotateError("api_status", error)), Effect.mapError((error) => new ConnectorError({
47
+ message: `Linear API returned ${response.status} for ${operation}`,
48
+ cause: error
49
+ })));
50
+ const errors = body.errors ?? [];
51
+ if (errors.some((error) => error.extensions?.code === "RATELIMITED")) return yield* new RateLimited({ message: `Linear rate limit reached for ${operation}` });
52
+ if (response.status === 401) return yield* new ConnectorError({ message: "Linear rejected the API key" });
53
+ if (errors.length > 0 && errors.every((error) => isNotFound(error.message))) return Option.none();
54
+ const [first] = errors;
55
+ if (first !== void 0 || response.status < 200 || response.status >= 300) {
56
+ const error = new ConnectorError({
57
+ message: `Linear API returned ${response.status} for ${operation}${first ? `: ${first.message}` : ""}`,
58
+ cause: errors
59
+ });
60
+ yield* Telemetry.annotateError("api_graphql", error);
61
+ return yield* error;
62
+ }
63
+ return Option.some(yield* Schema.decodeUnknownEffect(schema)(body.data).pipe(Effect.tapError((error) => Telemetry.annotateError("api_decode", error)), Effect.mapError((error) => new ConnectorError({
64
+ message: `Linear API response for ${operation} does not match the schema`,
65
+ cause: error
66
+ }))));
67
+ }).pipe(Effect.timeoutOrElse({
68
+ duration: requestTimeout,
69
+ orElse: () => Effect.fail(new ConnectorError({ message: `Linear API request timed out for ${operation}` }))
70
+ }), Effect.retry({
71
+ schedule: limitRetry.pipe(Schedule.tap(({ duration }) => Effect.logWarning("Linear rate limit reached, waiting to try again").pipe(Effect.annotateLogs({
72
+ operation,
73
+ waitMillis: Duration.toMillis(duration)
74
+ })))),
75
+ while: (error) => error._tag === "RateLimited"
76
+ }), Effect.catchTag("RateLimited", (error) => Effect.fail(new ConnectorError({ message: error.message }))), Effect.withSpan(Telemetry.SpanName.apiFetch, {
77
+ kind: "client",
78
+ attributes: { [Telemetry.Attr.apiPath]: `graphql:${operation}` }
79
+ }));
80
+ };
81
+ const query = (schema, document, variables) => execute(schema, document, variables).pipe(Effect.flatMap(Option.match({
82
+ onNone: () => Effect.fail(new ConnectorError({ message: `Linear API found nothing for ${operationName(document)}` })),
83
+ onSome: Effect.succeed
84
+ })));
85
+ return LinearClient.of({
86
+ query,
87
+ find: execute
88
+ });
89
+ });
90
+ const layer$1 = (config) => Layer.effect(LinearClient)(make$1(config));
91
+ const layerConfig$1 = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer$1)));
92
+ //#endregion
93
+ //#region src/schemas/shared.ts
94
+ const Timestamp = Schema.DateFromString;
95
+ /** Decodes a GraphQL Int as bigint for an Iceberg long. */
96
+ const Long = Schema.Int.pipe(Schema.decodeTo(Schema.BigInt, SchemaTransformation.transform({
97
+ decode: (value) => BigInt(value),
98
+ encode: (value) => Number(value)
99
+ })));
100
+ /** Decodes a linked object such as `assignee { id }` as its ID. */
101
+ const Ref = Schema.Struct({ id: Schema.String }).pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({
102
+ decode: (ref) => ref.id,
103
+ encode: (id) => ({ id })
104
+ })));
105
+ const OptionalRef = Schema.NullOr(Ref);
106
+ const OptionalTimestamp = Schema.NullOr(Timestamp);
107
+ const OptionalString = Schema.NullOr(Schema.String);
108
+ /** Decodes connection nodes as IDs. The query must fetch any extra pages. */
109
+ const RefList = Schema.Struct({ nodes: Schema.Array(Schema.Struct({ id: Schema.String })) }).pipe(Schema.decodeTo(Schema.Array(Schema.String), SchemaTransformation.transform({
110
+ decode: (connection) => connection.nodes.map((node) => node.id),
111
+ encode: (ids) => ({ nodes: ids.map((id) => ({ id })) })
112
+ })));
113
+ /** Rows use updated_at as version; lifecycle webhooks use the webhook time. */
114
+ const common = (object) => ({
115
+ id: Schema.String.pipe(Schema.annotateKey({ description: `${object} ID.` })),
116
+ version: Schema.Date.pipe(Schema.annotateKey({ description: `Time Linear last changed the ${object.toLowerCase()}.` })),
117
+ created_at: Timestamp.pipe(Schema.annotateKey({ description: `Time the ${object.toLowerCase()} was created.` })),
118
+ updated_at: Timestamp.pipe(Schema.annotateKey({ description: `Time the ${object.toLowerCase()} was last updated.` })),
119
+ archived_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: `Time the ${object.toLowerCase()} was archived or trashed.` }))
120
+ });
121
+ const deleted = (object) => Schema.optional(Schema.Boolean).pipe(Schema.annotateKey({ description: `Whether the ${object} was deleted in Linear.` }));
122
+ //#endregion
123
+ //#region src/schemas/comments.ts
124
+ const CommentSchema = Schema.Struct({
125
+ ...common("Comment"),
126
+ _deleted: deleted("comment"),
127
+ body: Schema.String.pipe(Schema.annotateKey({ description: "Comment text in markdown." })),
128
+ user_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who wrote the comment." })),
129
+ external_user_id: OptionalRef.pipe(Schema.annotateKey({ description: "External user who wrote the comment." })),
130
+ issue_id: OptionalString.pipe(Schema.annotateKey({ description: "Issue the comment is on." })),
131
+ project_id: OptionalString.pipe(Schema.annotateKey({ description: "Project the comment is on." })),
132
+ project_update_id: OptionalString.pipe(Schema.annotateKey({ description: "Project update the comment is on." })),
133
+ initiative_id: OptionalString.pipe(Schema.annotateKey({ description: "Initiative the comment is on." })),
134
+ initiative_update_id: OptionalString.pipe(Schema.annotateKey({ description: "Initiative update the comment is on." })),
135
+ document_content_id: OptionalString.pipe(Schema.annotateKey({ description: "Document content the comment is on." })),
136
+ parent_id: OptionalString.pipe(Schema.annotateKey({ description: "Comment this one replies to." })),
137
+ quoted_text: OptionalString.pipe(Schema.annotateKey({ description: "Text the comment quotes." })),
138
+ edited_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the comment was last edited." })),
139
+ resolved_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the thread was resolved." })),
140
+ resolving_user_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who resolved the thread." })),
141
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Comment URL." }))
142
+ }).annotate({ description: "Linear comments on issues, projects, initiatives, and updates." }).pipe(Iceberg.identifier("id"));
143
+ const CommentObjectSchema = CommentSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
144
+ //#endregion
145
+ //#region src/resources/pages.ts
146
+ const changesInterval = Duration.minutes(5);
147
+ const hourly = Duration.hours(1);
148
+ const maxPagesPerRun = 20;
149
+ const changesOverlap = Duration.minutes(1);
150
+ /** Keeps item types inferred in the callbacks. */
151
+ const listSpec = (spec) => spec;
152
+ const rowsOf = (toRow) => (items) => Effect.succeed(items.map(toRow));
153
+ const PageInfoSchema = Schema.Struct({
154
+ hasNextPage: Schema.Boolean,
155
+ endCursor: Schema.NullOr(Schema.String)
156
+ });
157
+ const readPage = (client, spec, options) => client.query(Schema.Struct({ page: Schema.Struct({
158
+ nodes: Schema.Array(spec.item),
159
+ pageInfo: PageInfoSchema
160
+ }) }).pipe(Schema.encodeKeys({ page: spec.connection })), `query ${String$1.capitalize(spec.connection)}($first: Int!, $after: String, $orderBy: PaginationOrderBy) {
161
+ ${spec.connection}(first: $first, after: $after, orderBy: $orderBy, includeArchived: true) {
162
+ nodes { ${spec.fields} }
163
+ pageInfo { hasNextPage endCursor }
164
+ }
165
+ }`, {
166
+ first: options.first ?? spec.first ?? 250,
167
+ after: Option.getOrNull(options.after),
168
+ orderBy: options.orderBy
169
+ }).pipe(Effect.map(({ page }) => ({
170
+ items: page.nodes,
171
+ next: page.pageInfo.hasNextPage ? Option.fromNullishOr(page.pageInfo.endCursor) : Option.none()
172
+ })));
173
+ const listAll = (client, spec) => Stream.paginate(Option.none(), (after) => readPage(client, spec, {
174
+ orderBy: "createdAt",
175
+ after
176
+ }).pipe(Effect.map(({ items, next }) => [items, Option.map(next, Option.some)]))).pipe(Stream.runCollect);
177
+ const checkList = (client, spec) => readPage(client, spec, {
178
+ orderBy: "createdAt",
179
+ after: Option.none(),
180
+ first: 1
181
+ }).pipe(Effect.flatMap(({ items }) => spec.rows(items)), Effect.asVoid);
182
+ /** Small lists are read in full every hour. */
183
+ const listSources = (client, spec) => {
184
+ const read = listAll(client, spec).pipe(Effect.flatMap(spec.rows));
185
+ return {
186
+ check: checkList(client, spec),
187
+ backfill: Fetch.page({
188
+ pageCursor: Cursor.string(),
189
+ cutoff: Cursor.isoDateTime(),
190
+ fetch: () => read.pipe(Effect.map((rows) => ({
191
+ rows,
192
+ hasMore: false
193
+ })))
194
+ }),
195
+ changes: Fetch.changes({
196
+ cursor: Cursor.string(),
197
+ interval: hourly,
198
+ fetch: () => Effect.all([read, DateTime.now]).pipe(Effect.map(([rows, now]) => ({
199
+ rows,
200
+ cursor: DateTime.formatIso(now)
201
+ })))
202
+ })
203
+ };
204
+ };
205
+ /** Changes pick up items created during backfill. */
206
+ const backfillList = (client, spec) => Fetch.page({
207
+ pageCursor: Cursor.string(),
208
+ cutoff: Cursor.isoDateTime(),
209
+ fetch: ({ pageCursor }) => Effect.gen(function* () {
210
+ const page = yield* readPage(client, spec, {
211
+ orderBy: "createdAt",
212
+ after: Option.fromNullishOr(typeof pageCursor === "string" ? pageCursor : void 0)
213
+ });
214
+ return {
215
+ rows: yield* spec.rows(page.items),
216
+ hasMore: Option.isSome(page.next),
217
+ ...Option.match(page.next, {
218
+ onNone: () => ({}),
219
+ onSome: (nextPageCursor) => ({ nextPageCursor })
220
+ })
221
+ };
222
+ })
223
+ });
224
+ /**
225
+ * `since` is when the last run started. `pending` is set while a run is split:
226
+ * `until` is the start of that run, and `after` the page to go on from.
227
+ */
228
+ const ChangesCursorSchema = Schema.Struct({
229
+ since: Schema.DateTimeUtcFromString,
230
+ pending: Schema.optional(Schema.Struct({
231
+ until: Schema.DateTimeUtcFromString,
232
+ after: Schema.String
233
+ }))
234
+ });
235
+ const ChangesCursorCodec = Schema.Union([Schema.fromJsonString(ChangesCursorSchema), Schema.DateTimeUtcFromString.pipe(Schema.decodeTo(Schema.toType(ChangesCursorSchema), SchemaTransformation.transform({
236
+ decode: (since) => ({ since }),
237
+ encode: (cursor) => cursor.since
238
+ })))]);
239
+ const cursorError = (cause) => new ConnectorError({
240
+ message: "Invalid Linear changes cursor",
241
+ cause
242
+ });
243
+ /**
244
+ * Newest first: an edit during paging is read again on the next run.
245
+ */
246
+ const changesList = (client, spec) => Fetch.changes({
247
+ cursor: Cursor.string(),
248
+ interval: changesInterval,
249
+ fetch: ({ cursor }) => Effect.gen(function* () {
250
+ const state = yield* Schema.decodeUnknownEffect(ChangesCursorCodec)(String(cursor)).pipe(Effect.mapError(cursorError));
251
+ const until = state.pending?.until ?? (yield* DateTime.now);
252
+ const from = DateTime.toDateUtc(DateTime.subtractDuration(state.since, changesOverlap));
253
+ const isRecent = (item) => spec.updatedAt(item).getTime() >= from.getTime();
254
+ const pages = yield* Stream.paginate(Option.fromNullishOr(state.pending?.after), (after) => Effect.gen(function* () {
255
+ const page = yield* readPage(client, spec, {
256
+ orderBy: "updatedAt",
257
+ after
258
+ });
259
+ const recent = page.items.filter(isRecent);
260
+ const next = recent.length === page.items.length ? page.next : Option.none();
261
+ return [[{
262
+ rows: yield* spec.rows(recent),
263
+ next
264
+ }], Option.map(next, Option.some)];
265
+ })).pipe(Stream.take(maxPagesPerRun), Stream.runCollect);
266
+ const rows = pages.flatMap((page) => page.rows);
267
+ const after = Option.flatMap(Array.last(pages), (page) => page.next);
268
+ const next = pages.length === maxPagesPerRun && Option.isSome(after) ? {
269
+ since: state.since,
270
+ pending: {
271
+ until,
272
+ after: after.value
273
+ }
274
+ } : { since: until };
275
+ return {
276
+ rows,
277
+ cursor: yield* Schema.encodeEffect(Schema.fromJsonString(ChangesCursorSchema))(next).pipe(Effect.mapError(cursorError)),
278
+ hasMore: next.pending !== void 0
279
+ };
280
+ })
281
+ });
282
+ const WebhookPayloadSchema = Schema.Struct({
283
+ data: Schema.Struct({ id: Schema.String }),
284
+ webhookTimestamp: Schema.Number
285
+ });
286
+ /**
287
+ * Archive and trash also send `remove`, without changing `updatedAt`.
288
+ * Read the item again; delete only if it's gone. Use the webhook time as the version.
289
+ */
290
+ const webhookList = (client, spec) => ({
291
+ schema: WebhookPayloadSchema,
292
+ handler: ({ payload }) => Effect.gen(function* () {
293
+ const version = new Date(payload.webhookTimestamp);
294
+ const single = spec.single ?? spec.connection;
295
+ const found = yield* client.find(Schema.Struct({ item: spec.item }).pipe(Schema.encodeKeys({ item: single })), `query ${String$1.capitalize(single)}($id: String!) {
296
+ ${single}(id: $id) { ${spec.fields} }
297
+ }`, { id: payload.data.id });
298
+ return yield* Option.match(found, {
299
+ onNone: () => Effect.succeed([{
300
+ id: payload.data.id,
301
+ version,
302
+ _deleted: true
303
+ }]),
304
+ onSome: ({ item }) => spec.rows([item]).pipe(Effect.map((rows) => rows.map((row) => ({
305
+ ...row,
306
+ version
307
+ }))))
308
+ });
309
+ })
310
+ });
311
+ //#endregion
312
+ //#region src/resources/comments.ts
313
+ const commentSpec = listSpec({
314
+ connection: "comments",
315
+ single: "comment",
316
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
317
+ body user_id: user { id } external_user_id: externalUser { id }
318
+ issue_id: issueId project_id: projectId project_update_id: projectUpdateId
319
+ initiative_id: initiativeId initiative_update_id: initiativeUpdateId
320
+ document_content_id: documentContentId parent_id: parentId quoted_text: quotedText
321
+ edited_at: editedAt resolved_at: resolvedAt resolving_user_id: resolvingUser { id } url`,
322
+ item: CommentObjectSchema,
323
+ updatedAt: (comment) => comment.updated_at,
324
+ rows: rowsOf((comment) => ({
325
+ ...comment,
326
+ version: comment.updated_at,
327
+ _deleted: false
328
+ }))
329
+ });
330
+ const makeComments = (client) => Resource.entity({
331
+ name: "comments",
332
+ rowSchema: CommentSchema,
333
+ version: "version",
334
+ check: checkList(client, commentSpec),
335
+ backfill: backfillList(client, commentSpec),
336
+ changes: changesList(client, commentSpec),
337
+ webhook: webhookList(client, commentSpec)
338
+ });
339
+ //#endregion
340
+ //#region src/schemas/customers.ts
341
+ const CustomerSchema = Schema.Struct({
342
+ ...common("Customer"),
343
+ _deleted: deleted("customer"),
344
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Customer name." })),
345
+ domains: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Email domains of the customer." })),
346
+ external_ids: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "IDs in other systems." })),
347
+ logo_url: OptionalString.pipe(Schema.annotateKey({ description: "Logo URL." })),
348
+ owner_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who owns the customer." })),
349
+ status_id: Ref.pipe(Schema.annotateKey({ description: "Customer status." })),
350
+ tier_id: OptionalRef.pipe(Schema.annotateKey({ description: "Customer tier." })),
351
+ revenue: Schema.NullOr(Long).pipe(Schema.annotateKey({ description: "Annual revenue." })),
352
+ size: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Company size." })),
353
+ approximate_need_count: Schema.Finite.pipe(Schema.annotateKey({ description: "About how many requests it has." })),
354
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Customer URL." }))
355
+ }).annotate({ description: "Linear customers." }).pipe(Iceberg.identifier("id"));
356
+ const CustomerObjectSchema = CustomerSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
357
+ const CustomerNeedSchema = Schema.Struct({
358
+ ...common("Request"),
359
+ _deleted: deleted("request"),
360
+ customer_id: OptionalRef.pipe(Schema.annotateKey({ description: "Customer." })),
361
+ issue_id: OptionalRef.pipe(Schema.annotateKey({ description: "Issue the request is linked to." })),
362
+ project_id: OptionalRef.pipe(Schema.annotateKey({ description: "Project the request is linked to." })),
363
+ original_issue_id: OptionalRef.pipe(Schema.annotateKey({ description: "Issue the request was first linked to." })),
364
+ comment_id: OptionalRef.pipe(Schema.annotateKey({ description: "Comment the request came from." })),
365
+ attachment_id: OptionalRef.pipe(Schema.annotateKey({ description: "Attachment the request came from." })),
366
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who added the request." })),
367
+ body: OptionalString.pipe(Schema.annotateKey({ description: "Request text in markdown." })),
368
+ priority: Schema.Finite.pipe(Schema.annotateKey({ description: "0 normal, 1 important." })),
369
+ url: OptionalString.pipe(Schema.annotateKey({ description: "Source URL." }))
370
+ }).annotate({ description: "Linear customer requests, linked to issues or projects." }).pipe(Iceberg.identifier("id"));
371
+ const CustomerNeedObjectSchema = CustomerNeedSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
372
+ const levelFields$1 = (object) => ({
373
+ ...common(object),
374
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Name." })),
375
+ display_name: Schema.String.pipe(Schema.annotateKey({ description: "Name shown in Linear." })),
376
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Description." })),
377
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Color, as hex." })),
378
+ position: Schema.Finite.pipe(Schema.annotateKey({ description: "Order in the list." }))
379
+ });
380
+ const CustomerStatusSchema = Schema.Struct(levelFields$1("Status")).annotate({ description: "Linear customer statuses. Names the IDs in `customers.status_id`." }).pipe(Iceberg.identifier("id"));
381
+ const CustomerStatusObjectSchema = CustomerStatusSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
382
+ const CustomerTierSchema = Schema.Struct(levelFields$1("Tier")).annotate({ description: "Linear customer tiers. Names the IDs in `customers.tier_id`." }).pipe(Iceberg.identifier("id"));
383
+ const CustomerTierObjectSchema = CustomerTierSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
384
+ //#endregion
385
+ //#region src/resources/customers.ts
386
+ const customerSpec = listSpec({
387
+ connection: "customers",
388
+ single: "customer",
389
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
390
+ name domains external_ids: externalIds logo_url: logoUrl owner_id: owner { id }
391
+ status_id: status { id } tier_id: tier { id } revenue size
392
+ approximate_need_count: approximateNeedCount url`,
393
+ item: CustomerObjectSchema,
394
+ updatedAt: (customer) => customer.updated_at,
395
+ rows: rowsOf((customer) => ({
396
+ ...customer,
397
+ version: customer.updated_at,
398
+ _deleted: false
399
+ }))
400
+ });
401
+ const makeCustomers = (client) => Resource.entity({
402
+ name: "customers",
403
+ rowSchema: CustomerSchema,
404
+ version: "version",
405
+ check: checkList(client, customerSpec),
406
+ backfill: backfillList(client, customerSpec),
407
+ changes: changesList(client, customerSpec),
408
+ webhook: webhookList(client, customerSpec)
409
+ });
410
+ const customerNeedSpec = listSpec({
411
+ connection: "customerNeeds",
412
+ single: "customerNeed",
413
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
414
+ customer_id: customer { id } issue_id: issue { id } project_id: project { id }
415
+ original_issue_id: originalIssue { id } comment_id: comment { id }
416
+ attachment_id: attachment { id } creator_id: creator { id } body priority url`,
417
+ item: CustomerNeedObjectSchema,
418
+ updatedAt: (need) => need.updated_at,
419
+ rows: rowsOf((need) => ({
420
+ ...need,
421
+ version: need.updated_at,
422
+ _deleted: false
423
+ }))
424
+ });
425
+ const makeCustomerNeeds = (client) => Resource.entity({
426
+ name: "customer_needs",
427
+ rowSchema: CustomerNeedSchema,
428
+ version: "version",
429
+ check: checkList(client, customerNeedSpec),
430
+ backfill: backfillList(client, customerNeedSpec),
431
+ changes: changesList(client, customerNeedSpec),
432
+ webhook: webhookList(client, customerNeedSpec)
433
+ });
434
+ const levelFields = `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt name display_name: displayName description color position`;
435
+ const customerStatusSpec = listSpec({
436
+ connection: "customerStatuses",
437
+ fields: levelFields,
438
+ item: CustomerStatusObjectSchema,
439
+ updatedAt: (status) => status.updated_at,
440
+ rows: rowsOf((status) => ({
441
+ ...status,
442
+ version: status.updated_at
443
+ }))
444
+ });
445
+ const makeCustomerStatuses = (client) => Resource.entity({
446
+ name: "customer_statuses",
447
+ rowSchema: CustomerStatusSchema,
448
+ version: "version",
449
+ ...listSources(client, customerStatusSpec)
450
+ });
451
+ const customerTierSpec = listSpec({
452
+ connection: "customerTiers",
453
+ fields: levelFields,
454
+ item: CustomerTierObjectSchema,
455
+ updatedAt: (tier) => tier.updated_at,
456
+ rows: rowsOf((tier) => ({
457
+ ...tier,
458
+ version: tier.updated_at
459
+ }))
460
+ });
461
+ const makeCustomerTiers = (client) => Resource.entity({
462
+ name: "customer_tiers",
463
+ rowSchema: CustomerTierSchema,
464
+ version: "version",
465
+ ...listSources(client, customerTierSpec)
466
+ });
467
+ //#endregion
468
+ //#region src/schemas/documents.ts
469
+ const DocumentSchema = Schema.Struct({
470
+ ...common("Document"),
471
+ _deleted: deleted("document"),
472
+ title: Schema.String.pipe(Schema.annotateKey({ description: "Title." })),
473
+ content: OptionalString.pipe(Schema.annotateKey({ description: "Content in markdown." })),
474
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Document URL." })),
475
+ icon: OptionalString.pipe(Schema.annotateKey({ description: "Icon." })),
476
+ color: OptionalString.pipe(Schema.annotateKey({ description: "Icon color." })),
477
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the document." })),
478
+ updated_by_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who last changed the document." })),
479
+ project_id: OptionalRef.pipe(Schema.annotateKey({ description: "Project the document is in." })),
480
+ initiative_id: OptionalRef.pipe(Schema.annotateKey({ description: "Initiative the document is in." })),
481
+ issue_id: OptionalRef.pipe(Schema.annotateKey({ description: "Issue the document is in." })),
482
+ cycle_id: OptionalRef.pipe(Schema.annotateKey({ description: "Cycle the document is in." })),
483
+ team_id: OptionalRef.pipe(Schema.annotateKey({ description: "Team the document is in." })),
484
+ parent_document_id: OptionalRef.pipe(Schema.annotateKey({ description: "Parent document." })),
485
+ hidden_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the document was hidden." })),
486
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the document is in the trash." }))
487
+ }).annotate({ description: "Linear documents, including archived and trashed ones." }).pipe(Iceberg.identifier("id"));
488
+ //#endregion
489
+ //#region src/resources/documents.ts
490
+ const documentSpec = listSpec({
491
+ connection: "documents",
492
+ single: "document",
493
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
494
+ title content url icon color creator_id: creator { id } updated_by_id: updatedBy { id }
495
+ project_id: project { id } initiative_id: initiative { id } issue_id: issue { id }
496
+ cycle_id: cycle { id } team_id: team { id } parent_document_id: parentDocument { id }
497
+ hidden_at: hiddenAt trashed`,
498
+ item: DocumentSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"])),
499
+ updatedAt: (document) => document.updated_at,
500
+ rows: rowsOf((document) => ({
501
+ ...document,
502
+ version: document.updated_at,
503
+ _deleted: false
504
+ }))
505
+ });
506
+ const makeDocuments = (client) => Resource.entity({
507
+ name: "documents",
508
+ rowSchema: DocumentSchema,
509
+ version: "version",
510
+ check: checkList(client, documentSpec),
511
+ backfill: backfillList(client, documentSpec),
512
+ changes: changesList(client, documentSpec),
513
+ webhook: webhookList(client, documentSpec)
514
+ });
515
+ //#endregion
516
+ //#region src/schemas/initiatives.ts
517
+ const InitiativeSchema = Schema.Struct({
518
+ ...common("Initiative"),
519
+ _deleted: deleted("initiative"),
520
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Initiative name." })),
521
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Short summary." })),
522
+ content: OptionalString.pipe(Schema.annotateKey({ description: "Initiative overview in markdown." })),
523
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Initiative URL." })),
524
+ icon: OptionalString.pipe(Schema.annotateKey({ description: "Icon." })),
525
+ color: OptionalString.pipe(Schema.annotateKey({ description: "Color, as hex." })),
526
+ status: Schema.String.pipe(Schema.annotateKey({ description: "Status: Planned, Active, or Completed." })),
527
+ health: OptionalString.pipe(Schema.annotateKey({ description: "Health from the latest update." })),
528
+ health_updated_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the health last changed." })),
529
+ priority: Long.pipe(Schema.annotateKey({ description: "0 none, 1 urgent, 2 high, 3 medium, 4 low." })),
530
+ owner_id: OptionalRef.pipe(Schema.annotateKey({ description: "Initiative owner." })),
531
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the initiative." })),
532
+ parent_initiative_id: OptionalRef.pipe(Schema.annotateKey({ description: "Parent initiative." })),
533
+ target_date: OptionalString.pipe(Schema.annotateKey({ description: "Target date, as YYYY-MM-DD." })),
534
+ started_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the initiative started." })),
535
+ completed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the initiative was completed." })),
536
+ canceled_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the initiative was canceled." })),
537
+ label_ids: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Initiative labels." })),
538
+ visibility: Schema.String.pipe(Schema.annotateKey({ description: "Who can see the initiative." })),
539
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the initiative is in the trash." }))
540
+ }).annotate({ description: "Linear initiatives, including archived and trashed ones." }).pipe(Iceberg.identifier("id"));
541
+ const InitiativeObjectSchema = InitiativeSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
542
+ const InitiativeUpdateSchema = Schema.Struct({
543
+ ...common("Update"),
544
+ _deleted: deleted("update"),
545
+ initiative_id: Ref.pipe(Schema.annotateKey({ description: "Initiative." })),
546
+ user_id: Ref.pipe(Schema.annotateKey({ description: "User who posted the update." })),
547
+ body: Schema.String.pipe(Schema.annotateKey({ description: "Update text in markdown." })),
548
+ health: Schema.String.pipe(Schema.annotateKey({ description: "Health: onTrack, atRisk, or offTrack." })),
549
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Update URL." })),
550
+ edited_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the update was last edited." })),
551
+ comment_count: Long.pipe(Schema.annotateKey({ description: "Comments on the update." }))
552
+ }).annotate({ description: "Linear initiative updates: status posts with a health." }).pipe(Iceberg.identifier("id"));
553
+ const InitiativeUpdateObjectSchema = InitiativeUpdateSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
554
+ const InitiativeLabelSchema = Schema.Struct({
555
+ ...common("Label"),
556
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Label name." })),
557
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Label description." })),
558
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Label color, as hex." })),
559
+ is_group: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the label groups other labels." })),
560
+ parent_id: OptionalRef.pipe(Schema.annotateKey({ description: "Label group." })),
561
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the label." })),
562
+ retired_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the label was retired." }))
563
+ }).annotate({ description: "Linear initiative labels. Names the IDs in `initiatives.label_ids`." }).pipe(Iceberg.identifier("id"));
564
+ const InitiativeLabelObjectSchema = InitiativeLabelSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
565
+ const InitiativeRelationSchema = Schema.Struct({
566
+ ...common("Relation"),
567
+ initiative_id: Ref.pipe(Schema.annotateKey({ description: "Initiative the relation starts from." })),
568
+ related_initiative_id: Ref.pipe(Schema.annotateKey({ description: "Related initiative." })),
569
+ sort_order: Schema.Finite.pipe(Schema.annotateKey({ description: "Order." })),
570
+ user_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the relation." }))
571
+ }).annotate({ description: "Linear initiative relations. Deleted relations aren't seen." }).pipe(Iceberg.identifier("id"));
572
+ const InitiativeRelationObjectSchema = InitiativeRelationSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
573
+ const InitiativeToProjectSchema = Schema.Struct({
574
+ ...common("Link"),
575
+ initiative_id: Ref.pipe(Schema.annotateKey({ description: "Initiative." })),
576
+ project_id: Ref.pipe(Schema.annotateKey({ description: "Project." })),
577
+ sort_order: Schema.String.pipe(Schema.annotateKey({ description: "Order within the initiative." }))
578
+ }).annotate({ description: "Links between Linear initiatives and projects. Removed links aren't seen." }).pipe(Iceberg.identifier("id"));
579
+ const InitiativeToProjectObjectSchema = InitiativeToProjectSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
580
+ //#endregion
581
+ //#region src/resources/initiatives.ts
582
+ const initiativeSpec = listSpec({
583
+ connection: "initiatives",
584
+ single: "initiative",
585
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
586
+ name description content url icon color status health
587
+ health_updated_at: healthUpdatedAt priority owner_id: owner { id } creator_id: creator { id }
588
+ parent_initiative_id: parentInitiative { id } target_date: targetDate
589
+ started_at: startedAt completed_at: completedAt canceled_at: canceledAt
590
+ label_ids: labelIds visibility trashed`,
591
+ item: InitiativeObjectSchema,
592
+ updatedAt: (initiative) => initiative.updated_at,
593
+ rows: rowsOf((initiative) => ({
594
+ ...initiative,
595
+ version: initiative.updated_at,
596
+ _deleted: false
597
+ }))
598
+ });
599
+ const makeInitiatives = (client) => Resource.entity({
600
+ name: "initiatives",
601
+ rowSchema: InitiativeSchema,
602
+ version: "version",
603
+ check: checkList(client, initiativeSpec),
604
+ backfill: backfillList(client, initiativeSpec),
605
+ changes: changesList(client, initiativeSpec),
606
+ webhook: webhookList(client, initiativeSpec)
607
+ });
608
+ const initiativeUpdateSpec = listSpec({
609
+ connection: "initiativeUpdates",
610
+ single: "initiativeUpdate",
611
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
612
+ initiative_id: initiative { id } user_id: user { id } body health url
613
+ edited_at: editedAt comment_count: commentCount`,
614
+ item: InitiativeUpdateObjectSchema,
615
+ updatedAt: (update) => update.updated_at,
616
+ rows: rowsOf((update) => ({
617
+ ...update,
618
+ version: update.updated_at,
619
+ _deleted: false
620
+ }))
621
+ });
622
+ const makeInitiativeUpdates = (client) => Resource.entity({
623
+ name: "initiative_updates",
624
+ rowSchema: InitiativeUpdateSchema,
625
+ version: "version",
626
+ check: checkList(client, initiativeUpdateSpec),
627
+ backfill: backfillList(client, initiativeUpdateSpec),
628
+ changes: changesList(client, initiativeUpdateSpec),
629
+ webhook: webhookList(client, initiativeUpdateSpec)
630
+ });
631
+ const initiativeLabelSpec = listSpec({
632
+ connection: "initiativeLabels",
633
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
634
+ name description color is_group: isGroup parent_id: parent { id }
635
+ creator_id: creator { id } retired_at: retiredAt`,
636
+ item: InitiativeLabelObjectSchema,
637
+ updatedAt: (label) => label.updated_at,
638
+ rows: rowsOf((label) => ({
639
+ ...label,
640
+ version: label.updated_at
641
+ }))
642
+ });
643
+ const makeInitiativeLabels = (client) => Resource.entity({
644
+ name: "initiative_labels",
645
+ rowSchema: InitiativeLabelSchema,
646
+ version: "version",
647
+ ...listSources(client, initiativeLabelSpec)
648
+ });
649
+ const initiativeRelationSpec = listSpec({
650
+ connection: "initiativeRelations",
651
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
652
+ initiative_id: initiative { id } related_initiative_id: relatedInitiative { id }
653
+ sort_order: sortOrder user_id: user { id }`,
654
+ item: InitiativeRelationObjectSchema,
655
+ updatedAt: (relation) => relation.updated_at,
656
+ rows: rowsOf((relation) => ({
657
+ ...relation,
658
+ version: relation.updated_at
659
+ }))
660
+ });
661
+ const makeInitiativeRelations = (client) => Resource.entity({
662
+ name: "initiative_relations",
663
+ rowSchema: InitiativeRelationSchema,
664
+ version: "version",
665
+ check: checkList(client, initiativeRelationSpec),
666
+ backfill: backfillList(client, initiativeRelationSpec),
667
+ changes: changesList(client, initiativeRelationSpec)
668
+ });
669
+ const initiativeToProjectSpec = listSpec({
670
+ connection: "initiativeToProjects",
671
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt initiative_id: initiative { id } project_id: project { id } sort_order: sortOrder`,
672
+ item: InitiativeToProjectObjectSchema,
673
+ updatedAt: (link) => link.updated_at,
674
+ rows: rowsOf((link) => ({
675
+ ...link,
676
+ version: link.updated_at
677
+ }))
678
+ });
679
+ const makeInitiativeToProjects = (client) => Resource.entity({
680
+ name: "initiative_to_projects",
681
+ rowSchema: InitiativeToProjectSchema,
682
+ version: "version",
683
+ check: checkList(client, initiativeToProjectSpec),
684
+ backfill: backfillList(client, initiativeToProjectSpec),
685
+ changes: changesList(client, initiativeToProjectSpec)
686
+ });
687
+ //#endregion
688
+ //#region src/schemas/issues.ts
689
+ const IssueSchema = Schema.Struct({
690
+ ...common("Issue"),
691
+ _deleted: deleted("issue"),
692
+ identifier: Schema.String.pipe(Schema.annotateKey({ description: "Readable ID, such as ENG-123." })),
693
+ number: Schema.Finite.pipe(Schema.annotateKey({ description: "Issue number within its team." })),
694
+ title: Schema.String.pipe(Schema.annotateKey({ description: "Issue title." })),
695
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Description in markdown." })),
696
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Issue URL." })),
697
+ branch_name: Schema.String.pipe(Schema.annotateKey({ description: "Suggested Git branch name." })),
698
+ priority: Schema.Finite.pipe(Schema.annotateKey({ description: "0 none, 1 urgent, 2 high, 3 medium, 4 low." })),
699
+ priority_label: Schema.String.pipe(Schema.annotateKey({ description: "Priority name." })),
700
+ estimate: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Estimate, on the team's scale." })),
701
+ due_date: OptionalString.pipe(Schema.annotateKey({ description: "Due date, as YYYY-MM-DD." })),
702
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who created the issue." })),
703
+ assignee_id: OptionalRef.pipe(Schema.annotateKey({ description: "Assigned user." })),
704
+ delegate_id: OptionalRef.pipe(Schema.annotateKey({ description: "Agent user working on the issue." })),
705
+ snoozed_by_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who snoozed the issue." })),
706
+ state_id: Ref.pipe(Schema.annotateKey({ description: "Workflow state." })),
707
+ team_id: Ref.pipe(Schema.annotateKey({ description: "Team the issue belongs to." })),
708
+ project_id: OptionalRef.pipe(Schema.annotateKey({ description: "Project." })),
709
+ project_milestone_id: OptionalRef.pipe(Schema.annotateKey({ description: "Project milestone." })),
710
+ cycle_id: OptionalRef.pipe(Schema.annotateKey({ description: "Cycle." })),
711
+ parent_id: OptionalRef.pipe(Schema.annotateKey({ description: "Parent issue." })),
712
+ source_comment_id: OptionalRef.pipe(Schema.annotateKey({ description: "Comment the issue was made from." })),
713
+ external_user_creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "External user who created the issue through an integration." })),
714
+ label_ids: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Labels." })),
715
+ previous_identifiers: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Earlier readable IDs, from before the issue moved team." })),
716
+ started_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue moved to a started state." })),
717
+ completed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was completed." })),
718
+ canceled_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was canceled." })),
719
+ started_triage_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue entered triage." })),
720
+ triaged_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue left triage." })),
721
+ auto_closed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was closed automatically." })),
722
+ auto_archived_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was archived automatically." })),
723
+ added_to_cycle_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was added to its cycle." })),
724
+ added_to_project_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was added to its project." })),
725
+ added_to_team_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue was added to its team." })),
726
+ snoozed_until_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the issue is snoozed until." })),
727
+ sla_type: OptionalString.pipe(Schema.annotateKey({ description: "SLA type." })),
728
+ sla_started_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the SLA started." })),
729
+ sla_breaches_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the SLA breaches." })),
730
+ customer_ticket_count: Long.pipe(Schema.annotateKey({ description: "Linked support tickets." })),
731
+ integration_source_type: OptionalString.pipe(Schema.annotateKey({ description: "Integration that created the issue." })),
732
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the issue is in the trash." }))
733
+ }).annotate({ description: "Linear issues, including archived and trashed ones." }).pipe(Iceberg.identifier("id"));
734
+ const IssueObjectSchema = IssueSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
735
+ const IssueHistorySchema = Schema.Struct({
736
+ ...common("History entry"),
737
+ issue_id: Schema.String.pipe(Schema.annotateKey({ description: "Issue that changed." })),
738
+ actor_id: OptionalString.pipe(Schema.annotateKey({ description: "User who made the change." })),
739
+ from_state_id: OptionalString.pipe(Schema.annotateKey({ description: "Workflow state before." })),
740
+ to_state_id: OptionalString.pipe(Schema.annotateKey({ description: "Workflow state after." })),
741
+ from_assignee_id: OptionalString.pipe(Schema.annotateKey({ description: "Assignee before." })),
742
+ to_assignee_id: OptionalString.pipe(Schema.annotateKey({ description: "Assignee after." })),
743
+ from_cycle_id: OptionalString.pipe(Schema.annotateKey({ description: "Cycle before." })),
744
+ to_cycle_id: OptionalString.pipe(Schema.annotateKey({ description: "Cycle after." })),
745
+ from_project_id: OptionalString.pipe(Schema.annotateKey({ description: "Project before." })),
746
+ to_project_id: OptionalString.pipe(Schema.annotateKey({ description: "Project after." })),
747
+ from_parent_id: OptionalString.pipe(Schema.annotateKey({ description: "Parent issue before." })),
748
+ to_parent_id: OptionalString.pipe(Schema.annotateKey({ description: "Parent issue after." })),
749
+ from_team_id: OptionalString.pipe(Schema.annotateKey({ description: "Team before." })),
750
+ to_team_id: OptionalString.pipe(Schema.annotateKey({ description: "Team after." })),
751
+ from_priority: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Priority before." })),
752
+ to_priority: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Priority after." })),
753
+ from_estimate: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Estimate before." })),
754
+ to_estimate: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Estimate after." })),
755
+ from_due_date: OptionalString.pipe(Schema.annotateKey({ description: "Due date before, as YYYY-MM-DD." })),
756
+ to_due_date: OptionalString.pipe(Schema.annotateKey({ description: "Due date after, as YYYY-MM-DD." })),
757
+ from_title: OptionalString.pipe(Schema.annotateKey({ description: "Title before." })),
758
+ to_title: OptionalString.pipe(Schema.annotateKey({ description: "Title after." })),
759
+ added_label_ids: Schema.NullOr(Schema.Array(Schema.String)).pipe(Schema.annotateKey({ description: "Labels added." })),
760
+ removed_label_ids: Schema.NullOr(Schema.Array(Schema.String)).pipe(Schema.annotateKey({ description: "Labels removed." })),
761
+ attachment_id: OptionalString.pipe(Schema.annotateKey({ description: "Attachment linked or unlinked." })),
762
+ customer_need_id: OptionalString.pipe(Schema.annotateKey({ description: "Customer request linked or unlinked." })),
763
+ to_converted_project_id: OptionalString.pipe(Schema.annotateKey({ description: "Project the issue became." })),
764
+ updated_description: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the description changed." })),
765
+ archived: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the issue was archived." })),
766
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the issue was trashed." })),
767
+ auto_archived: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the issue was archived automatically." })),
768
+ auto_closed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the issue was closed automatically." }))
769
+ }).annotate({ description: "Linear issue history. One row per change to an issue." }).pipe(Iceberg.identifier("id"));
770
+ const IssueHistoryObjectSchema = IssueHistorySchema.mapFields((fields) => Struct.omit(fields, ["version", "issue_id"]));
771
+ const IssueRelationSchema = Schema.Struct({
772
+ ...common("Relation"),
773
+ issue_id: Ref.pipe(Schema.annotateKey({ description: "Issue the relation starts from." })),
774
+ related_issue_id: Ref.pipe(Schema.annotateKey({ description: "Related issue." })),
775
+ type: Schema.String.pipe(Schema.annotateKey({ description: "Relation type: blocks, duplicate, related, or similar." }))
776
+ }).annotate({ description: "Linear issue relations. Deleted relations aren't seen." }).pipe(Iceberg.identifier("id"));
777
+ const IssueRelationObjectSchema = IssueRelationSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
778
+ const AttachmentSchema = Schema.Struct({
779
+ ...common("Attachment"),
780
+ _deleted: deleted("attachment"),
781
+ issue_id: Ref.pipe(Schema.annotateKey({ description: "Issue the attachment is on." })),
782
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who added it." })),
783
+ external_user_creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "External user who added it." })),
784
+ title: Schema.String.pipe(Schema.annotateKey({ description: "Title." })),
785
+ subtitle: OptionalString.pipe(Schema.annotateKey({ description: "Subtitle." })),
786
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Linked URL, such as a pull request or ticket." })),
787
+ source_type: OptionalString.pipe(Schema.annotateKey({ description: "Source, such as github or zendesk." }))
788
+ }).annotate({ description: "Linear issue attachments: links to pull requests, tickets, and more." }).pipe(Iceberg.identifier("id"));
789
+ const AttachmentObjectSchema = AttachmentSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
790
+ const IssueLabelSchema = Schema.Struct({
791
+ ...common("Label"),
792
+ _deleted: deleted("label"),
793
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Label name." })),
794
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Label description." })),
795
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Label color, as hex." })),
796
+ is_group: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the label groups other labels." })),
797
+ parent_id: OptionalRef.pipe(Schema.annotateKey({ description: "Label group." })),
798
+ team_id: OptionalRef.pipe(Schema.annotateKey({ description: "Team, or null for a workspace label." })),
799
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the label." })),
800
+ retired_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the label was retired." }))
801
+ }).annotate({ description: "Linear issue labels. Names the IDs in `issues.label_ids`." }).pipe(Iceberg.identifier("id"));
802
+ const IssueLabelObjectSchema = IssueLabelSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
803
+ //#endregion
804
+ //#region src/resources/issue-history.ts
805
+ const historyFields = `
806
+ id created_at: createdAt updated_at: updatedAt archived_at: archivedAt actor_id: actorId
807
+ from_state_id: fromStateId to_state_id: toStateId
808
+ from_assignee_id: fromAssigneeId to_assignee_id: toAssigneeId
809
+ from_cycle_id: fromCycleId to_cycle_id: toCycleId
810
+ from_project_id: fromProjectId to_project_id: toProjectId
811
+ from_parent_id: fromParentId to_parent_id: toParentId from_team_id: fromTeamId to_team_id: toTeamId
812
+ from_priority: fromPriority to_priority: toPriority
813
+ from_estimate: fromEstimate to_estimate: toEstimate
814
+ from_due_date: fromDueDate to_due_date: toDueDate from_title: fromTitle to_title: toTitle
815
+ added_label_ids: addedLabelIds removed_label_ids: removedLabelIds
816
+ attachment_id: attachmentId customer_need_id: customerNeedId
817
+ to_converted_project_id: toConvertedProjectId updated_description: updatedDescription
818
+ archived trashed auto_archived: autoArchived auto_closed: autoClosed`;
819
+ const HistoryPageSchema = Schema.Struct({
820
+ nodes: Schema.Array(IssueHistoryObjectSchema),
821
+ pageInfo: Schema.Struct({
822
+ hasNextPage: Schema.Boolean,
823
+ endCursor: Schema.NullOr(Schema.String)
824
+ })
825
+ });
826
+ const IssueWithHistorySchema = Schema.Struct({
827
+ id: Schema.String,
828
+ updated_at: Timestamp,
829
+ history: HistoryPageSchema
830
+ });
831
+ const nextCursor = ({ pageInfo }) => pageInfo.hasNextPage ? Option.fromNullishOr(pageInfo.endCursor) : Option.none();
832
+ /** Returns the history after the first page, without repeating that page. */
833
+ const readMore = (client, issueId, first) => Option.match(nextCursor(first), {
834
+ onNone: () => Effect.succeed([]),
835
+ onSome: (cursor) => Stream.paginate(cursor, (after) => client.query(Schema.Struct({ issue: Schema.Struct({ history: HistoryPageSchema }) }), `query IssueHistory($id: String!, $after: String) {
836
+ issue(id: $id) { history(first: 250, after: $after) { nodes { ${historyFields} } pageInfo { hasNextPage endCursor } } }
837
+ }`, {
838
+ id: issueId,
839
+ after
840
+ }).pipe(Effect.map(({ issue }) => [issue.history.nodes, nextCursor(issue.history)]))).pipe(Stream.runCollect)
841
+ });
842
+ /** Linear has no top-level history query. */
843
+ const historySpec = (client) => listSpec({
844
+ connection: "issues",
845
+ fields: `id updated_at: updatedAt history(first: 50) { nodes { ${historyFields} } pageInfo { hasNextPage endCursor } }`,
846
+ item: IssueWithHistorySchema,
847
+ first: 25,
848
+ updatedAt: (issue) => issue.updated_at,
849
+ rows: (issues) => Effect.forEach(issues, (issue) => readMore(client, issue.id, issue.history).pipe(Effect.map((more) => [...issue.history.nodes, ...more].map((entry) => ({
850
+ ...entry,
851
+ issue_id: issue.id,
852
+ version: entry.updated_at
853
+ }))))).pipe(Effect.map((rows) => rows.flat()))
854
+ });
855
+ const makeIssueHistory = (client) => {
856
+ const spec = historySpec(client);
857
+ return Resource.entity({
858
+ name: "issue_history",
859
+ rowSchema: IssueHistorySchema,
860
+ version: "version",
861
+ check: checkList(client, spec),
862
+ backfill: backfillList(client, spec),
863
+ changes: changesList(client, spec),
864
+ webhook: {
865
+ schema: Schema.Struct({ data: Schema.Struct({ id: Schema.String }) }),
866
+ handler: ({ payload }) => client.find(Schema.Struct({ issue: IssueWithHistorySchema }), `query IssueHistory($id: String!) {
867
+ issue(id: $id) { ${spec.fields} }
868
+ }`, { id: payload.data.id }).pipe(Effect.flatMap(Option.match({
869
+ onNone: () => Effect.succeed([]),
870
+ onSome: ({ issue }) => spec.rows([issue])
871
+ })))
872
+ }
873
+ });
874
+ };
875
+ //#endregion
876
+ //#region src/resources/issues.ts
877
+ const issueSpec = listSpec({
878
+ connection: "issues",
879
+ single: "issue",
880
+ fields: `
881
+ id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
882
+ identifier number title description url branch_name: branchName
883
+ priority priority_label: priorityLabel estimate due_date: dueDate
884
+ creator_id: creator { id } assignee_id: assignee { id } delegate_id: delegate { id }
885
+ snoozed_by_id: snoozedBy { id } state_id: state { id } team_id: team { id }
886
+ project_id: project { id } project_milestone_id: projectMilestone { id }
887
+ cycle_id: cycle { id } parent_id: parent { id } source_comment_id: sourceComment { id }
888
+ external_user_creator_id: externalUserCreator { id }
889
+ label_ids: labelIds previous_identifiers: previousIdentifiers
890
+ started_at: startedAt completed_at: completedAt canceled_at: canceledAt
891
+ started_triage_at: startedTriageAt triaged_at: triagedAt auto_closed_at: autoClosedAt
892
+ auto_archived_at: autoArchivedAt added_to_cycle_at: addedToCycleAt
893
+ added_to_project_at: addedToProjectAt added_to_team_at: addedToTeamAt
894
+ snoozed_until_at: snoozedUntilAt sla_type: slaType sla_started_at: slaStartedAt
895
+ sla_breaches_at: slaBreachesAt customer_ticket_count: customerTicketCount
896
+ integration_source_type: integrationSourceType trashed`,
897
+ item: IssueObjectSchema,
898
+ updatedAt: (issue) => issue.updated_at,
899
+ rows: rowsOf((issue) => ({
900
+ ...issue,
901
+ version: issue.updated_at,
902
+ _deleted: false
903
+ }))
904
+ });
905
+ const makeIssues = (client) => Resource.entity({
906
+ name: "issues",
907
+ rowSchema: IssueSchema,
908
+ version: "version",
909
+ check: checkList(client, issueSpec),
910
+ backfill: backfillList(client, issueSpec),
911
+ changes: changesList(client, issueSpec),
912
+ webhook: webhookList(client, issueSpec)
913
+ });
914
+ const issueRelationSpec = listSpec({
915
+ connection: "issueRelations",
916
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
917
+ issue_id: issue { id } related_issue_id: relatedIssue { id } type`,
918
+ item: IssueRelationObjectSchema,
919
+ updatedAt: (relation) => relation.updated_at,
920
+ rows: rowsOf((relation) => ({
921
+ ...relation,
922
+ version: relation.updated_at
923
+ }))
924
+ });
925
+ const makeIssueRelations = (client) => Resource.entity({
926
+ name: "issue_relations",
927
+ rowSchema: IssueRelationSchema,
928
+ version: "version",
929
+ check: checkList(client, issueRelationSpec),
930
+ backfill: backfillList(client, issueRelationSpec),
931
+ changes: changesList(client, issueRelationSpec)
932
+ });
933
+ const attachmentSpec = listSpec({
934
+ connection: "attachments",
935
+ single: "attachment",
936
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
937
+ issue_id: issue { id } creator_id: creator { id }
938
+ external_user_creator_id: externalUserCreator { id }
939
+ title subtitle url source_type: sourceType`,
940
+ item: AttachmentObjectSchema,
941
+ updatedAt: (attachment) => attachment.updated_at,
942
+ rows: rowsOf((attachment) => ({
943
+ ...attachment,
944
+ version: attachment.updated_at,
945
+ _deleted: false
946
+ }))
947
+ });
948
+ const makeAttachments = (client) => Resource.entity({
949
+ name: "attachments",
950
+ rowSchema: AttachmentSchema,
951
+ version: "version",
952
+ check: checkList(client, attachmentSpec),
953
+ backfill: backfillList(client, attachmentSpec),
954
+ changes: changesList(client, attachmentSpec),
955
+ webhook: webhookList(client, attachmentSpec)
956
+ });
957
+ const issueLabelSpec = listSpec({
958
+ connection: "issueLabels",
959
+ single: "issueLabel",
960
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
961
+ name description color is_group: isGroup parent_id: parent { id } team_id: team { id }
962
+ creator_id: creator { id } retired_at: retiredAt`,
963
+ item: IssueLabelObjectSchema,
964
+ updatedAt: (label) => label.updated_at,
965
+ rows: rowsOf((label) => ({
966
+ ...label,
967
+ version: label.updated_at,
968
+ _deleted: false
969
+ }))
970
+ });
971
+ const makeIssueLabels = (client) => Resource.entity({
972
+ name: "issue_labels",
973
+ rowSchema: IssueLabelSchema,
974
+ version: "version",
975
+ ...listSources(client, issueLabelSpec),
976
+ webhook: webhookList(client, issueLabelSpec)
977
+ });
978
+ //#endregion
979
+ //#region src/schemas/projects.ts
980
+ const ProjectSchema = Schema.Struct({
981
+ ...common("Project"),
982
+ _deleted: deleted("project"),
983
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Project name." })),
984
+ description: Schema.String.pipe(Schema.annotateKey({ description: "Short summary." })),
985
+ content: OptionalString.pipe(Schema.annotateKey({ description: "Project overview in markdown." })),
986
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Project URL." })),
987
+ icon: OptionalString.pipe(Schema.annotateKey({ description: "Icon." })),
988
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Color, as hex." })),
989
+ status_id: Ref.pipe(Schema.annotateKey({ description: "Project status." })),
990
+ priority: Long.pipe(Schema.annotateKey({ description: "0 none, 1 urgent, 2 high, 3 medium, 4 low." })),
991
+ priority_label: Schema.String.pipe(Schema.annotateKey({ description: "Priority name." })),
992
+ health: OptionalString.pipe(Schema.annotateKey({ description: "Health from the latest update." })),
993
+ health_updated_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the health last changed." })),
994
+ progress: Schema.Finite.pipe(Schema.annotateKey({ description: "Share of the scope completed, from 0 to 1." })),
995
+ scope: Schema.Finite.pipe(Schema.annotateKey({ description: "Total estimate of the project's issues." })),
996
+ lead_id: OptionalRef.pipe(Schema.annotateKey({ description: "Project lead." })),
997
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the project." })),
998
+ converted_from_issue_id: OptionalRef.pipe(Schema.annotateKey({ description: "Issue the project was made from." })),
999
+ start_date: OptionalString.pipe(Schema.annotateKey({ description: "Start date, as YYYY-MM-DD." })),
1000
+ target_date: OptionalString.pipe(Schema.annotateKey({ description: "Target date, as YYYY-MM-DD." })),
1001
+ started_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the project started." })),
1002
+ completed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the project was completed." })),
1003
+ canceled_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the project was canceled." })),
1004
+ auto_archived_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the project was archived automatically." })),
1005
+ label_ids: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Project labels." })),
1006
+ team_ids: RefList.pipe(Schema.annotateKey({ description: "Teams the project belongs to. Reads up to 50." })),
1007
+ member_ids: RefList.pipe(Schema.annotateKey({ description: "Project members. Reads up to 100." })),
1008
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the project is in the trash." }))
1009
+ }).annotate({ description: "Linear projects, including archived and trashed ones." }).pipe(Iceberg.identifier("id"));
1010
+ const ProjectObjectSchema = ProjectSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
1011
+ const ProjectMilestoneSchema = Schema.Struct({
1012
+ ...common("Milestone"),
1013
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Milestone name." })),
1014
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Description in markdown." })),
1015
+ project_id: Ref.pipe(Schema.annotateKey({ description: "Project." })),
1016
+ target_date: OptionalString.pipe(Schema.annotateKey({ description: "Target date, as YYYY-MM-DD." })),
1017
+ status: Schema.String.pipe(Schema.annotateKey({ description: "Status: unstarted, next, overdue, or done." })),
1018
+ progress: Schema.Finite.pipe(Schema.annotateKey({ description: "Share of the scope completed, from 0 to 1." })),
1019
+ sort_order: Schema.Finite.pipe(Schema.annotateKey({ description: "Order within the project." }))
1020
+ }).annotate({ description: "Linear project milestones. Deleted milestones aren't seen." }).pipe(Iceberg.identifier("id"));
1021
+ const ProjectMilestoneObjectSchema = ProjectMilestoneSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1022
+ const ProjectUpdateSchema = Schema.Struct({
1023
+ ...common("Update"),
1024
+ _deleted: deleted("update"),
1025
+ project_id: Ref.pipe(Schema.annotateKey({ description: "Project." })),
1026
+ user_id: Ref.pipe(Schema.annotateKey({ description: "User who posted the update." })),
1027
+ body: Schema.String.pipe(Schema.annotateKey({ description: "Update text in markdown." })),
1028
+ health: Schema.String.pipe(Schema.annotateKey({ description: "Health: onTrack, atRisk, or offTrack." })),
1029
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Update URL." })),
1030
+ edited_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the update was last edited." })),
1031
+ comment_count: Long.pipe(Schema.annotateKey({ description: "Comments on the update." }))
1032
+ }).annotate({ description: "Linear project updates: status posts with a health." }).pipe(Iceberg.identifier("id"));
1033
+ const ProjectUpdateObjectSchema = ProjectUpdateSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
1034
+ const ProjectStatusSchema = Schema.Struct({
1035
+ ...common("Status"),
1036
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Status name." })),
1037
+ type: Schema.String.pipe(Schema.annotateKey({ description: "Status type: backlog, planned, started, paused, completed, or canceled." })),
1038
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Color, as hex." })),
1039
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Description." })),
1040
+ position: Schema.Finite.pipe(Schema.annotateKey({ description: "Order in the list." }))
1041
+ }).annotate({ description: "Linear project statuses. Names the IDs in `projects.status_id`." }).pipe(Iceberg.identifier("id"));
1042
+ const ProjectStatusObjectSchema = ProjectStatusSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1043
+ const ProjectLabelSchema = Schema.Struct({
1044
+ ...common("Label"),
1045
+ _deleted: deleted("label"),
1046
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Label name." })),
1047
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Label description." })),
1048
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Label color, as hex." })),
1049
+ is_group: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the label groups other labels." })),
1050
+ parent_id: OptionalRef.pipe(Schema.annotateKey({ description: "Label group." })),
1051
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the label." })),
1052
+ retired_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the label was retired." }))
1053
+ }).annotate({ description: "Linear project labels. Names the IDs in `projects.label_ids`." }).pipe(Iceberg.identifier("id"));
1054
+ const ProjectLabelObjectSchema = ProjectLabelSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
1055
+ const ProjectRelationSchema = Schema.Struct({
1056
+ ...common("Relation"),
1057
+ type: Schema.String.pipe(Schema.annotateKey({ description: "Relation type, such as dependency." })),
1058
+ project_id: Ref.pipe(Schema.annotateKey({ description: "Project the relation starts from." })),
1059
+ project_milestone_id: OptionalRef.pipe(Schema.annotateKey({ description: "Milestone the relation starts from." })),
1060
+ anchor_type: Schema.String.pipe(Schema.annotateKey({ description: "Which end of the project it starts from." })),
1061
+ related_project_id: Ref.pipe(Schema.annotateKey({ description: "Related project." })),
1062
+ related_project_milestone_id: OptionalRef.pipe(Schema.annotateKey({ description: "Related milestone." })),
1063
+ related_anchor_type: Schema.String.pipe(Schema.annotateKey({ description: "Which end of the related project." })),
1064
+ user_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the relation." }))
1065
+ }).annotate({ description: "Linear project dependencies. Deleted relations aren't seen." }).pipe(Iceberg.identifier("id"));
1066
+ const ProjectRelationObjectSchema = ProjectRelationSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1067
+ //#endregion
1068
+ //#region src/resources/projects.ts
1069
+ const projectSpec = listSpec({
1070
+ connection: "projects",
1071
+ single: "project",
1072
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1073
+ name description content url icon color status_id: status { id } priority
1074
+ priority_label: priorityLabel health health_updated_at: healthUpdatedAt progress scope
1075
+ lead_id: lead { id } creator_id: creator { id }
1076
+ converted_from_issue_id: convertedFromIssue { id } start_date: startDate
1077
+ target_date: targetDate started_at: startedAt completed_at: completedAt
1078
+ canceled_at: canceledAt auto_archived_at: autoArchivedAt label_ids: labelIds
1079
+ team_ids: teams(first: 50) { nodes { id } } member_ids: members(first: 100) { nodes { id } }
1080
+ trashed`,
1081
+ item: ProjectObjectSchema,
1082
+ first: 50,
1083
+ updatedAt: (project) => project.updated_at,
1084
+ rows: rowsOf((project) => ({
1085
+ ...project,
1086
+ version: project.updated_at,
1087
+ _deleted: false
1088
+ }))
1089
+ });
1090
+ const makeProjects = (client) => Resource.entity({
1091
+ name: "projects",
1092
+ rowSchema: ProjectSchema,
1093
+ version: "version",
1094
+ check: checkList(client, projectSpec),
1095
+ backfill: backfillList(client, projectSpec),
1096
+ changes: changesList(client, projectSpec),
1097
+ webhook: webhookList(client, projectSpec)
1098
+ });
1099
+ const projectMilestoneSpec = listSpec({
1100
+ connection: "projectMilestones",
1101
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1102
+ name description project_id: project { id } target_date: targetDate status progress
1103
+ sort_order: sortOrder`,
1104
+ item: ProjectMilestoneObjectSchema,
1105
+ updatedAt: (milestone) => milestone.updated_at,
1106
+ rows: rowsOf((milestone) => ({
1107
+ ...milestone,
1108
+ version: milestone.updated_at
1109
+ }))
1110
+ });
1111
+ const makeProjectMilestones = (client) => Resource.entity({
1112
+ name: "project_milestones",
1113
+ rowSchema: ProjectMilestoneSchema,
1114
+ version: "version",
1115
+ check: checkList(client, projectMilestoneSpec),
1116
+ backfill: backfillList(client, projectMilestoneSpec),
1117
+ changes: changesList(client, projectMilestoneSpec)
1118
+ });
1119
+ const projectUpdateSpec = listSpec({
1120
+ connection: "projectUpdates",
1121
+ single: "projectUpdate",
1122
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1123
+ project_id: project { id } user_id: user { id } body health url edited_at: editedAt
1124
+ comment_count: commentCount`,
1125
+ item: ProjectUpdateObjectSchema,
1126
+ updatedAt: (update) => update.updated_at,
1127
+ rows: rowsOf((update) => ({
1128
+ ...update,
1129
+ version: update.updated_at,
1130
+ _deleted: false
1131
+ }))
1132
+ });
1133
+ const makeProjectUpdates = (client) => Resource.entity({
1134
+ name: "project_updates",
1135
+ rowSchema: ProjectUpdateSchema,
1136
+ version: "version",
1137
+ check: checkList(client, projectUpdateSpec),
1138
+ backfill: backfillList(client, projectUpdateSpec),
1139
+ changes: changesList(client, projectUpdateSpec),
1140
+ webhook: webhookList(client, projectUpdateSpec)
1141
+ });
1142
+ const projectStatusSpec = listSpec({
1143
+ connection: "projectStatuses",
1144
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt name type color description position`,
1145
+ item: ProjectStatusObjectSchema,
1146
+ updatedAt: (status) => status.updated_at,
1147
+ rows: rowsOf((status) => ({
1148
+ ...status,
1149
+ version: status.updated_at
1150
+ }))
1151
+ });
1152
+ const makeProjectStatuses = (client) => Resource.entity({
1153
+ name: "project_statuses",
1154
+ rowSchema: ProjectStatusSchema,
1155
+ version: "version",
1156
+ ...listSources(client, projectStatusSpec)
1157
+ });
1158
+ const projectLabelSpec = listSpec({
1159
+ connection: "projectLabels",
1160
+ single: "projectLabel",
1161
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1162
+ name description color is_group: isGroup parent_id: parent { id }
1163
+ creator_id: creator { id } retired_at: retiredAt`,
1164
+ item: ProjectLabelObjectSchema,
1165
+ updatedAt: (label) => label.updated_at,
1166
+ rows: rowsOf((label) => ({
1167
+ ...label,
1168
+ version: label.updated_at,
1169
+ _deleted: false
1170
+ }))
1171
+ });
1172
+ const makeProjectLabels = (client) => Resource.entity({
1173
+ name: "project_labels",
1174
+ rowSchema: ProjectLabelSchema,
1175
+ version: "version",
1176
+ ...listSources(client, projectLabelSpec),
1177
+ webhook: webhookList(client, projectLabelSpec)
1178
+ });
1179
+ const projectRelationSpec = listSpec({
1180
+ connection: "projectRelations",
1181
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1182
+ type project_id: project { id } project_milestone_id: projectMilestone { id }
1183
+ anchor_type: anchorType related_project_id: relatedProject { id }
1184
+ related_project_milestone_id: relatedProjectMilestone { id }
1185
+ related_anchor_type: relatedAnchorType user_id: user { id }`,
1186
+ item: ProjectRelationObjectSchema,
1187
+ updatedAt: (relation) => relation.updated_at,
1188
+ rows: rowsOf((relation) => ({
1189
+ ...relation,
1190
+ version: relation.updated_at
1191
+ }))
1192
+ });
1193
+ const makeProjectRelations = (client) => Resource.entity({
1194
+ name: "project_relations",
1195
+ rowSchema: ProjectRelationSchema,
1196
+ version: "version",
1197
+ check: checkList(client, projectRelationSpec),
1198
+ backfill: backfillList(client, projectRelationSpec),
1199
+ changes: changesList(client, projectRelationSpec)
1200
+ });
1201
+ //#endregion
1202
+ //#region src/schemas/releases.ts
1203
+ const ReleaseSchema = Schema.Struct({
1204
+ ...common("Release"),
1205
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Release name." })),
1206
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Description." })),
1207
+ release_version: OptionalString.pipe(Schema.annotateKey({ description: "Version, such as 1.2.0." })),
1208
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Release URL." })),
1209
+ commit_sha: OptionalString.pipe(Schema.annotateKey({ description: "Commit SHA." })),
1210
+ pipeline_id: Ref.pipe(Schema.annotateKey({ description: "Release pipeline." })),
1211
+ stage_id: Ref.pipe(Schema.annotateKey({ description: "Release stage." })),
1212
+ creator_id: OptionalRef.pipe(Schema.annotateKey({ description: "User who made the release." })),
1213
+ start_date: OptionalString.pipe(Schema.annotateKey({ description: "Start date, as YYYY-MM-DD." })),
1214
+ target_date: OptionalString.pipe(Schema.annotateKey({ description: "Target date, as YYYY-MM-DD." })),
1215
+ started_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the release started." })),
1216
+ completed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the release was completed." })),
1217
+ canceled_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the release was canceled." })),
1218
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the release is in the trash." }))
1219
+ }).annotate({ description: "Linear releases. Needs the Business plan." }).pipe(Iceberg.identifier("id"));
1220
+ const ReleaseObjectSchema = ReleaseSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1221
+ const ReleaseNoteSchema = Schema.Struct({
1222
+ ...common("Release note"),
1223
+ title: OptionalString.pipe(Schema.annotateKey({ description: "Release note title." })),
1224
+ content: Schema.NullOr(Schema.Struct({ content: OptionalString })).pipe(Schema.decodeTo(OptionalString, SchemaTransformation.transform({
1225
+ decode: (document) => document?.content ?? null,
1226
+ encode: (content) => content === null ? null : { content }
1227
+ })), Schema.annotateKey({ description: "Release note body in markdown." })),
1228
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Release note URL." })),
1229
+ pipeline_id: Ref.pipe(Schema.annotateKey({ description: "Release pipeline." })),
1230
+ release_ids: Schema.Array(Ref).pipe(Schema.annotateKey({ description: "Releases covered by the note." }))
1231
+ }).annotate({ description: "Linear release notes. Needs the Business plan." }).pipe(Iceberg.identifier("id"));
1232
+ const ReleaseNoteObjectSchema = ReleaseNoteSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1233
+ const ReleasePipelineSchema = Schema.Struct({
1234
+ ...common("Pipeline"),
1235
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Pipeline name." })),
1236
+ type: Schema.String.pipe(Schema.annotateKey({ description: "Pipeline type." })),
1237
+ is_production: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether it releases to production." })),
1238
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Pipeline URL." })),
1239
+ include_path_patterns: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "File paths that count toward the pipeline." })),
1240
+ approximate_release_count: Long.pipe(Schema.annotateKey({ description: "About how many releases it has." })),
1241
+ trashed: Schema.NullOr(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the pipeline is in the trash." }))
1242
+ }).annotate({ description: "Linear release pipelines. Needs the Business plan." }).pipe(Iceberg.identifier("id"));
1243
+ const ReleasePipelineObjectSchema = ReleasePipelineSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1244
+ const ReleaseStageSchema = Schema.Struct({
1245
+ ...common("Stage"),
1246
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Stage name." })),
1247
+ type: Schema.String.pipe(Schema.annotateKey({ description: "Stage type." })),
1248
+ color: Schema.String.pipe(Schema.annotateKey({ description: "Color, as hex." })),
1249
+ position: Schema.Finite.pipe(Schema.annotateKey({ description: "Order in the pipeline." })),
1250
+ frozen: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the stage is frozen." })),
1251
+ pipeline_id: Ref.pipe(Schema.annotateKey({ description: "Release pipeline." }))
1252
+ }).annotate({ description: "Linear release stages. Needs the Business plan." }).pipe(Iceberg.identifier("id"));
1253
+ const ReleaseStageObjectSchema = ReleaseStageSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1254
+ const IssueToReleaseSchema = Schema.Struct({
1255
+ ...common("Link"),
1256
+ issue_id: Ref.pipe(Schema.annotateKey({ description: "Issue." })),
1257
+ release_id: Ref.pipe(Schema.annotateKey({ description: "Release." }))
1258
+ }).annotate({ description: "Links between Linear issues and releases. Needs the Business plan." }).pipe(Iceberg.identifier("id"));
1259
+ const IssueToReleaseObjectSchema = IssueToReleaseSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1260
+ //#endregion
1261
+ //#region src/resources/releases.ts
1262
+ const releaseSpec = listSpec({
1263
+ connection: "releases",
1264
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1265
+ name description release_version: version url commit_sha: commitSha
1266
+ pipeline_id: pipeline { id } stage_id: stage { id } creator_id: creator { id }
1267
+ start_date: startDate target_date: targetDate started_at: startedAt
1268
+ completed_at: completedAt canceled_at: canceledAt trashed`,
1269
+ item: ReleaseObjectSchema,
1270
+ updatedAt: (release) => release.updated_at,
1271
+ rows: rowsOf((release) => ({
1272
+ ...release,
1273
+ version: release.updated_at
1274
+ }))
1275
+ });
1276
+ const makeReleases = (client) => Resource.entity({
1277
+ name: "releases",
1278
+ rowSchema: ReleaseSchema,
1279
+ version: "version",
1280
+ check: checkList(client, releaseSpec),
1281
+ backfill: backfillList(client, releaseSpec),
1282
+ changes: changesList(client, releaseSpec)
1283
+ });
1284
+ const releaseNoteSpec = listSpec({
1285
+ connection: "releaseNotes",
1286
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1287
+ title content: documentContent { content } url pipeline_id: pipeline { id }
1288
+ release_ids: releases { id }`,
1289
+ item: ReleaseNoteObjectSchema,
1290
+ updatedAt: (note) => note.updated_at,
1291
+ rows: rowsOf((note) => ({
1292
+ ...note,
1293
+ version: note.updated_at
1294
+ }))
1295
+ });
1296
+ const makeReleaseNotes = (client) => Resource.entity({
1297
+ name: "release_notes",
1298
+ rowSchema: ReleaseNoteSchema,
1299
+ version: "version",
1300
+ check: checkList(client, releaseNoteSpec),
1301
+ backfill: backfillList(client, releaseNoteSpec),
1302
+ changes: changesList(client, releaseNoteSpec)
1303
+ });
1304
+ const releasePipelineSpec = listSpec({
1305
+ connection: "releasePipelines",
1306
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1307
+ name type is_production: isProduction url include_path_patterns: includePathPatterns
1308
+ approximate_release_count: approximateReleaseCount trashed`,
1309
+ item: ReleasePipelineObjectSchema,
1310
+ updatedAt: (pipeline) => pipeline.updated_at,
1311
+ rows: rowsOf((pipeline) => ({
1312
+ ...pipeline,
1313
+ version: pipeline.updated_at
1314
+ }))
1315
+ });
1316
+ const makeReleasePipelines = (client) => Resource.entity({
1317
+ name: "release_pipelines",
1318
+ rowSchema: ReleasePipelineSchema,
1319
+ version: "version",
1320
+ ...listSources(client, releasePipelineSpec)
1321
+ });
1322
+ const releaseStageSpec = listSpec({
1323
+ connection: "releaseStages",
1324
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt name type color position frozen pipeline_id: pipeline { id }`,
1325
+ item: ReleaseStageObjectSchema,
1326
+ updatedAt: (stage) => stage.updated_at,
1327
+ rows: rowsOf((stage) => ({
1328
+ ...stage,
1329
+ version: stage.updated_at
1330
+ }))
1331
+ });
1332
+ const makeReleaseStages = (client) => Resource.entity({
1333
+ name: "release_stages",
1334
+ rowSchema: ReleaseStageSchema,
1335
+ version: "version",
1336
+ ...listSources(client, releaseStageSpec)
1337
+ });
1338
+ const issueToReleaseSpec = listSpec({
1339
+ connection: "issueToReleases",
1340
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt issue_id: issue { id } release_id: release { id }`,
1341
+ item: IssueToReleaseObjectSchema,
1342
+ updatedAt: (link) => link.updated_at,
1343
+ rows: rowsOf((link) => ({
1344
+ ...link,
1345
+ version: link.updated_at
1346
+ }))
1347
+ });
1348
+ const makeIssueToReleases = (client) => Resource.entity({
1349
+ name: "issue_to_releases",
1350
+ rowSchema: IssueToReleaseSchema,
1351
+ version: "version",
1352
+ check: checkList(client, issueToReleaseSpec),
1353
+ backfill: backfillList(client, issueToReleaseSpec),
1354
+ changes: changesList(client, issueToReleaseSpec)
1355
+ });
1356
+ //#endregion
1357
+ //#region src/schemas/teams.ts
1358
+ const TeamSchema = Schema.Struct({
1359
+ ...common("Team"),
1360
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Team name." })),
1361
+ key: Schema.String.pipe(Schema.annotateKey({ description: "Team key, used in issue IDs such as ENG-123." })),
1362
+ display_name: Schema.String.pipe(Schema.annotateKey({ description: "Name shown in Linear." })),
1363
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Team description." })),
1364
+ visibility: Schema.String.pipe(Schema.annotateKey({ description: "Who can see the team." })),
1365
+ parent_id: OptionalRef.pipe(Schema.annotateKey({ description: "Parent team." })),
1366
+ timezone: Schema.String.pipe(Schema.annotateKey({ description: "Team timezone." })),
1367
+ cycles_enabled: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the team uses cycles." })),
1368
+ cycle_duration: Schema.Finite.pipe(Schema.annotateKey({ description: "Cycle length in weeks." })),
1369
+ cycle_cooldown_time: Schema.Finite.pipe(Schema.annotateKey({ description: "Weeks between cycles." })),
1370
+ cycle_start_day: Schema.Finite.pipe(Schema.annotateKey({ description: "Day cycles start, 0 for Sunday." })),
1371
+ triage_enabled: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the team uses triage." })),
1372
+ issue_estimation_type: Schema.String.pipe(Schema.annotateKey({ description: "Estimate scale." })),
1373
+ default_issue_estimate: Schema.Finite.pipe(Schema.annotateKey({ description: "Default estimate." })),
1374
+ auto_archive_period: Schema.Finite.pipe(Schema.annotateKey({ description: "Months before closed issues are archived." })),
1375
+ auto_close_period: Schema.NullOr(Schema.Finite).pipe(Schema.annotateKey({ description: "Months before stale issues are closed." })),
1376
+ retired_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the team was retired." }))
1377
+ }).annotate({ description: "Linear teams." }).pipe(Iceberg.identifier("id"));
1378
+ const TeamObjectSchema = TeamSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1379
+ const TeamMembershipSchema = Schema.Struct({
1380
+ ...common("Membership"),
1381
+ team_id: Ref.pipe(Schema.annotateKey({ description: "Team." })),
1382
+ user_id: Ref.pipe(Schema.annotateKey({ description: "User." })),
1383
+ owner: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user owns the team." }))
1384
+ }).annotate({ description: "Linear team members. Removed members aren't seen." }).pipe(Iceberg.identifier("id"));
1385
+ const TeamMembershipObjectSchema = TeamMembershipSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1386
+ const WorkflowStateSchema = Schema.Struct({
1387
+ ...common("Workflow state"),
1388
+ name: Schema.String.pipe(Schema.annotateKey({ description: "State name." })),
1389
+ type: Schema.String.pipe(Schema.annotateKey({ description: "State type: triage, backlog, unstarted, started, completed, or canceled." })),
1390
+ color: Schema.String.pipe(Schema.annotateKey({ description: "State color, as hex." })),
1391
+ description: OptionalString.pipe(Schema.annotateKey({ description: "State description." })),
1392
+ position: Schema.Finite.pipe(Schema.annotateKey({ description: "Order within the team." })),
1393
+ team_id: Ref.pipe(Schema.annotateKey({ description: "Team." }))
1394
+ }).annotate({ description: "Linear workflow states. Names the IDs in `issues.state_id`." }).pipe(Iceberg.identifier("id"));
1395
+ const WorkflowStateObjectSchema = WorkflowStateSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1396
+ const UserSchema = Schema.Struct({
1397
+ ...common("User"),
1398
+ _deleted: deleted("user"),
1399
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Full name." })),
1400
+ display_name: Schema.String.pipe(Schema.annotateKey({ description: "Display name." })),
1401
+ email: Schema.String.pipe(Schema.annotateKey({ description: "Email address." })),
1402
+ avatar_url: OptionalString.pipe(Schema.annotateKey({ description: "Avatar image URL." })),
1403
+ title: OptionalString.pipe(Schema.annotateKey({ description: "Job title." })),
1404
+ timezone: OptionalString.pipe(Schema.annotateKey({ description: "Timezone." })),
1405
+ active: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user is active." })),
1406
+ admin: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user is an admin." })),
1407
+ owner: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user owns the workspace." })),
1408
+ guest: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user is a guest." })),
1409
+ app: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the user is an app or agent." })),
1410
+ last_seen: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the user was last online." })),
1411
+ url: Schema.String.pipe(Schema.annotateKey({ description: "Profile URL." }))
1412
+ }).annotate({ description: "Linear users." }).pipe(Iceberg.identifier("id"));
1413
+ const UserObjectSchema = UserSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
1414
+ const ExternalUserSchema = Schema.Struct({
1415
+ ...common("External user"),
1416
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Name." })),
1417
+ display_name: Schema.String.pipe(Schema.annotateKey({ description: "Display name." })),
1418
+ email: OptionalString.pipe(Schema.annotateKey({ description: "Email address." })),
1419
+ avatar_url: OptionalString.pipe(Schema.annotateKey({ description: "Avatar image URL." })),
1420
+ last_seen: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the user was last seen." }))
1421
+ }).annotate({ description: "People from integrations, such as Slack, who create issues and comments." }).pipe(Iceberg.identifier("id"));
1422
+ const ExternalUserObjectSchema = ExternalUserSchema.mapFields((fields) => Struct.omit(fields, ["version"]));
1423
+ const CycleSchema = Schema.Struct({
1424
+ ...common("Cycle"),
1425
+ _deleted: deleted("cycle"),
1426
+ number: Schema.Finite.pipe(Schema.annotateKey({ description: "Cycle number within its team." })),
1427
+ name: OptionalString.pipe(Schema.annotateKey({ description: "Cycle name." })),
1428
+ description: OptionalString.pipe(Schema.annotateKey({ description: "Cycle description." })),
1429
+ team_id: Ref.pipe(Schema.annotateKey({ description: "Team." })),
1430
+ starts_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the cycle starts." })),
1431
+ ends_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the cycle ends." })),
1432
+ completed_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the cycle was completed." })),
1433
+ auto_archived_at: OptionalTimestamp.pipe(Schema.annotateKey({ description: "Time the cycle was archived automatically." })),
1434
+ progress: Schema.Finite.pipe(Schema.annotateKey({ description: "Share of the scope completed, from 0 to 1." }))
1435
+ }).annotate({ description: "Linear cycles." }).pipe(Iceberg.identifier("id"));
1436
+ const CycleObjectSchema = CycleSchema.mapFields((fields) => Struct.omit(fields, ["version", "_deleted"]));
1437
+ //#endregion
1438
+ //#region src/resources/teams.ts
1439
+ const teamSpec = listSpec({
1440
+ connection: "teams",
1441
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1442
+ name key display_name: displayName description visibility parent_id: parent { id }
1443
+ timezone cycles_enabled: cyclesEnabled cycle_duration: cycleDuration
1444
+ cycle_cooldown_time: cycleCooldownTime cycle_start_day: cycleStartDay
1445
+ triage_enabled: triageEnabled issue_estimation_type: issueEstimationType
1446
+ default_issue_estimate: defaultIssueEstimate auto_archive_period: autoArchivePeriod
1447
+ auto_close_period: autoClosePeriod retired_at: retiredAt`,
1448
+ item: TeamObjectSchema,
1449
+ updatedAt: (team) => team.updated_at,
1450
+ rows: rowsOf((team) => ({
1451
+ ...team,
1452
+ version: team.updated_at
1453
+ }))
1454
+ });
1455
+ const makeTeams = (client) => Resource.entity({
1456
+ name: "teams",
1457
+ rowSchema: TeamSchema,
1458
+ version: "version",
1459
+ ...listSources(client, teamSpec)
1460
+ });
1461
+ const teamMembershipSpec = listSpec({
1462
+ connection: "teamMemberships",
1463
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt team_id: team { id } user_id: user { id } owner`,
1464
+ item: TeamMembershipObjectSchema,
1465
+ updatedAt: (membership) => membership.updated_at,
1466
+ rows: rowsOf((membership) => ({
1467
+ ...membership,
1468
+ version: membership.updated_at
1469
+ }))
1470
+ });
1471
+ const makeTeamMemberships = (client) => Resource.entity({
1472
+ name: "team_memberships",
1473
+ rowSchema: TeamMembershipSchema,
1474
+ version: "version",
1475
+ ...listSources(client, teamMembershipSpec)
1476
+ });
1477
+ const workflowStateSpec = listSpec({
1478
+ connection: "workflowStates",
1479
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt name type color description position team_id: team { id }`,
1480
+ item: WorkflowStateObjectSchema,
1481
+ updatedAt: (state) => state.updated_at,
1482
+ rows: rowsOf((state) => ({
1483
+ ...state,
1484
+ version: state.updated_at
1485
+ }))
1486
+ });
1487
+ const makeWorkflowStates = (client) => Resource.entity({
1488
+ name: "workflow_states",
1489
+ rowSchema: WorkflowStateSchema,
1490
+ version: "version",
1491
+ ...listSources(client, workflowStateSpec)
1492
+ });
1493
+ const userSpec = listSpec({
1494
+ connection: "users",
1495
+ single: "user",
1496
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1497
+ name display_name: displayName email avatar_url: avatarUrl title timezone
1498
+ active admin owner guest app last_seen: lastSeen url`,
1499
+ item: UserObjectSchema,
1500
+ updatedAt: (user) => user.updated_at,
1501
+ rows: rowsOf((user) => ({
1502
+ ...user,
1503
+ version: user.updated_at,
1504
+ _deleted: false
1505
+ }))
1506
+ });
1507
+ const makeUsers = (client) => Resource.entity({
1508
+ name: "users",
1509
+ rowSchema: UserSchema,
1510
+ version: "version",
1511
+ ...listSources(client, userSpec),
1512
+ webhook: webhookList(client, userSpec)
1513
+ });
1514
+ const externalUserSpec = listSpec({
1515
+ connection: "externalUsers",
1516
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt name display_name: displayName email avatar_url: avatarUrl last_seen: lastSeen`,
1517
+ item: ExternalUserObjectSchema,
1518
+ updatedAt: (user) => user.updated_at,
1519
+ rows: rowsOf((user) => ({
1520
+ ...user,
1521
+ version: user.updated_at
1522
+ }))
1523
+ });
1524
+ const makeExternalUsers = (client) => Resource.entity({
1525
+ name: "external_users",
1526
+ rowSchema: ExternalUserSchema,
1527
+ version: "version",
1528
+ ...listSources(client, externalUserSpec)
1529
+ });
1530
+ const cycleSpec = listSpec({
1531
+ connection: "cycles",
1532
+ single: "cycle",
1533
+ fields: `id created_at: createdAt updated_at: updatedAt archived_at: archivedAt
1534
+ number name description team_id: team { id } starts_at: startsAt ends_at: endsAt
1535
+ completed_at: completedAt auto_archived_at: autoArchivedAt progress`,
1536
+ item: CycleObjectSchema,
1537
+ updatedAt: (cycle) => cycle.updated_at,
1538
+ rows: rowsOf((cycle) => ({
1539
+ ...cycle,
1540
+ version: cycle.updated_at,
1541
+ _deleted: false
1542
+ }))
1543
+ });
1544
+ const makeCycles = (client) => Resource.entity({
1545
+ name: "cycles",
1546
+ rowSchema: CycleSchema,
1547
+ version: "version",
1548
+ check: checkList(client, cycleSpec),
1549
+ backfill: backfillList(client, cycleSpec),
1550
+ changes: changesList(client, cycleSpec),
1551
+ webhook: webhookList(client, cycleSpec)
1552
+ });
1553
+ //#endregion
1554
+ //#region src/webhook/signature.ts
1555
+ const maxAgeMillis = 6e4;
1556
+ const verifySignature = (options) => Effect.gen(function* () {
1557
+ const expected = createHmac("sha256", options.secret).update(options.rawBody).digest();
1558
+ const provided = Buffer.from(options.header ?? "", "hex");
1559
+ if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) return yield* new ConnectorError({ message: "Invalid Linear webhook signature" });
1560
+ const now = yield* DateTime.nowAsDate;
1561
+ if (options.timestamp === void 0 || Math.abs(now.getTime() - options.timestamp) > maxAgeMillis) return yield* new ConnectorError({ message: "Linear webhook is too old" });
1562
+ });
1563
+ //#endregion
1564
+ //#region src/webhook/route.ts
1565
+ const webhookPath = "/webhooks/linear";
1566
+ const WebhookEventSchema = Schema.Struct({
1567
+ type: Schema.String,
1568
+ action: Schema.String,
1569
+ webhookTimestamp: Schema.Number,
1570
+ data: Schema.Struct({ id: Schema.String })
1571
+ });
1572
+ const makeWebhookRoute = (options) => Webhook.route({
1573
+ path: webhookPath,
1574
+ ackMode: "after-enqueue",
1575
+ schema: WebhookEventSchema,
1576
+ handler: ({ request, rawBody, payload, to }) => Effect.gen(function* () {
1577
+ const verified = yield* verifySignature({
1578
+ rawBody,
1579
+ header: request.headers["linear-signature"],
1580
+ secret: options.secret,
1581
+ timestamp: payload.webhookTimestamp
1582
+ }).pipe(Effect.result);
1583
+ if (Result.isFailure(verified)) return HttpServerResponse.jsonUnsafe({
1584
+ ok: false,
1585
+ error: verified.failure.message
1586
+ }, { status: 401 });
1587
+ const resource = options.targets.get(payload.type);
1588
+ if (payload.action === "remove" || payload.action === "restore") {
1589
+ if (resource !== void 0) yield* to(resource, payload);
1590
+ if (payload.type === "Issue") yield* to(options.issueHistory, payload);
1591
+ }
1592
+ return HttpServerResponse.jsonUnsafe({ ok: true });
1593
+ })
1594
+ });
1595
+ //#endregion
1596
+ //#region src/connector.ts
1597
+ var connector_exports = /* @__PURE__ */ __exportAll({
1598
+ LinearConfigDef: () => LinearConfigDef,
1599
+ LinearConnector: () => LinearConnector,
1600
+ layer: () => layer,
1601
+ layerConfig: () => layerConfig,
1602
+ make: () => make,
1603
+ manifest: () => manifest
1604
+ });
1605
+ const make = Effect.fnUntraced(function* (config) {
1606
+ const client = yield* LinearClient;
1607
+ const Issues = makeIssues(client);
1608
+ const IssueHistory = makeIssueHistory(client);
1609
+ const Comments = makeComments(client);
1610
+ const Attachments = makeAttachments(client);
1611
+ const IssueLabels = makeIssueLabels(client);
1612
+ const Users = makeUsers(client);
1613
+ const Cycles = makeCycles(client);
1614
+ const Projects = makeProjects(client);
1615
+ const ProjectUpdates = makeProjectUpdates(client);
1616
+ const ProjectLabels = makeProjectLabels(client);
1617
+ const Initiatives = makeInitiatives(client);
1618
+ const InitiativeUpdates = makeInitiativeUpdates(client);
1619
+ const Documents = makeDocuments(client);
1620
+ const Customers = makeCustomers(client);
1621
+ const CustomerNeeds = makeCustomerNeeds(client);
1622
+ return Connector.define({
1623
+ name: "producer-linear",
1624
+ title: "Linear",
1625
+ resources: [
1626
+ Issues,
1627
+ IssueHistory,
1628
+ Comments,
1629
+ makeIssueRelations(client),
1630
+ Attachments,
1631
+ IssueLabels,
1632
+ makeWorkflowStates(client),
1633
+ makeTeams(client),
1634
+ makeTeamMemberships(client),
1635
+ Users,
1636
+ makeExternalUsers(client),
1637
+ Cycles,
1638
+ Projects,
1639
+ makeProjectMilestones(client),
1640
+ ProjectUpdates,
1641
+ makeProjectStatuses(client),
1642
+ ProjectLabels,
1643
+ makeProjectRelations(client),
1644
+ Initiatives,
1645
+ InitiativeUpdates,
1646
+ makeInitiativeLabels(client),
1647
+ makeInitiativeRelations(client),
1648
+ makeInitiativeToProjects(client),
1649
+ Documents,
1650
+ Customers,
1651
+ CustomerNeeds,
1652
+ makeCustomerStatuses(client),
1653
+ makeCustomerTiers(client),
1654
+ makeReleases(client),
1655
+ makeReleaseNotes(client),
1656
+ makeReleasePipelines(client),
1657
+ makeReleaseStages(client),
1658
+ makeIssueToReleases(client)
1659
+ ],
1660
+ webhooks: [makeWebhookRoute({
1661
+ secret: Redacted.value(config.webhookSecret),
1662
+ issueHistory: IssueHistory,
1663
+ targets: /* @__PURE__ */ new Map([
1664
+ ["Issue", Issues],
1665
+ ["Comment", Comments],
1666
+ ["Attachment", Attachments],
1667
+ ["IssueLabel", IssueLabels],
1668
+ ["User", Users],
1669
+ ["Cycle", Cycles],
1670
+ ["Project", Projects],
1671
+ ["ProjectUpdate", ProjectUpdates],
1672
+ ["ProjectLabel", ProjectLabels],
1673
+ ["Initiative", Initiatives],
1674
+ ["InitiativeUpdate", InitiativeUpdates],
1675
+ ["Document", Documents],
1676
+ ["Customer", Customers],
1677
+ ["CustomerNeed", CustomerNeeds]
1678
+ ])
1679
+ })]
1680
+ });
1681
+ });
1682
+ var LinearConnector = class extends Context.Service()("@useairfoil/producer-linear/LinearConnector") {};
1683
+ const layer = (config) => Layer.effect(LinearConnector)(make(config).pipe(Effect.annotateLogs({ component: "linear" }))).pipe(Layer.provide(layer$1(config)));
1684
+ const layerConfig = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer)));
1685
+ //#endregion
1686
+ export { InitiativeToProjectSchema as A, IssueHistorySchema as C, InitiativeLabelSchema as D, IssueSchema as E, CustomerStatusSchema as F, CustomerTierSchema as I, CommentSchema as L, DocumentSchema as M, CustomerNeedSchema as N, InitiativeRelationSchema as O, CustomerSchema as P, client_exports as R, AttachmentSchema as S, IssueRelationSchema as T, ProjectMilestoneSchema as _, CycleSchema as a, ProjectStatusSchema as b, TeamSchema as c, IssueToReleaseSchema as d, ReleaseNoteSchema as f, ProjectLabelSchema as g, ReleaseStageSchema as h, webhookPath as i, InitiativeUpdateSchema as j, InitiativeSchema as k, UserSchema as l, ReleaseSchema as m, connector_exports as n, ExternalUserSchema as o, ReleasePipelineSchema as p, layerConfig as r, TeamMembershipSchema as s, LinearConnector as t, WorkflowStateSchema as u, ProjectRelationSchema as v, IssueLabelSchema as w, ProjectUpdateSchema as x, ProjectSchema as y };
1687
+
1688
+ //# sourceMappingURL=src-BbzySPR7.mjs.map