@relayfile/adapter-linear 0.1.21 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/discovery/linear/.adapter.md +71 -0
- package/discovery/linear/issues/.create.example.json +6 -0
- package/discovery/linear/issues/.schema.json +145 -0
- package/discovery/linear/issues/{issueId}/comments/.create.example.json +3 -0
- package/discovery/linear/issues/{issueId}/comments/.schema.json +91 -0
- package/dist/__tests__/aliases.test.d.ts +2 -0
- package/dist/__tests__/aliases.test.d.ts.map +1 -0
- package/dist/__tests__/aliases.test.js +184 -0
- package/dist/__tests__/aliases.test.js.map +1 -0
- package/dist/__tests__/by-state.test.d.ts +2 -0
- package/dist/__tests__/by-state.test.d.ts.map +1 -0
- package/dist/__tests__/by-state.test.js +282 -0
- package/dist/__tests__/by-state.test.js.map +1 -0
- package/dist/__tests__/index-emission.test.d.ts +2 -0
- package/dist/__tests__/index-emission.test.d.ts.map +1 -0
- package/dist/__tests__/index-emission.test.js +118 -0
- package/dist/__tests__/index-emission.test.js.map +1 -0
- package/dist/__tests__/layout-prompt.test.d.ts +2 -0
- package/dist/__tests__/layout-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/layout-prompt.test.js +14 -0
- package/dist/__tests__/layout-prompt.test.js.map +1 -0
- package/dist/__tests__/linear-adapter.test.js +241 -7
- package/dist/__tests__/linear-adapter.test.js.map +1 -1
- package/dist/__tests__/name-id-convention.test.d.ts +2 -0
- package/dist/__tests__/name-id-convention.test.d.ts.map +1 -0
- package/dist/__tests__/name-id-convention.test.js +50 -0
- package/dist/__tests__/name-id-convention.test.js.map +1 -0
- package/dist/__tests__/path-mapper.test.js +5 -1
- package/dist/__tests__/path-mapper.test.js.map +1 -1
- package/dist/alias-slug.d.ts +3 -0
- package/dist/alias-slug.d.ts.map +1 -0
- package/dist/alias-slug.js +17 -0
- package/dist/alias-slug.js.map +1 -0
- package/dist/index-emitter.d.ts +10 -0
- package/dist/index-emitter.d.ts.map +1 -0
- package/dist/index-emitter.js +30 -0
- package/dist/index-emitter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layout-prompt.d.ts +7 -0
- package/dist/layout-prompt.d.ts.map +1 -0
- package/dist/layout-prompt.js +45 -0
- package/dist/layout-prompt.js.map +1 -0
- package/dist/linear-adapter.d.ts +11 -0
- package/dist/linear-adapter.d.ts.map +1 -1
- package/dist/linear-adapter.js +403 -33
- package/dist/linear-adapter.js.map +1 -1
- package/dist/path-mapper.d.ts +22 -3
- package/dist/path-mapper.d.ts.map +1 -1
- package/dist/path-mapper.js +133 -21
- package/dist/path-mapper.js.map +1 -1
- package/dist/queries.d.ts +24 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +50 -0
- package/dist/queries.js.map +1 -1
- package/dist/resources.d.ts +25 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +23 -0
- package/dist/resources.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback.d.ts +7 -3
- package/dist/writeback.d.ts.map +1 -1
- package/dist/writeback.js +52 -14
- package/dist/writeback.js.map +1 -1
- package/dist/writeback.test.js +15 -43
- package/dist/writeback.test.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const LINEAR_LAYOUT_PROMPT = `# Linear Mount Layout
|
|
2
|
+
|
|
3
|
+
Always run \`ls\` before constructing a path. PR 0 standardizes human-readable leaf names to \`<sanitized-name>__<id>\`, so consumers should inspect the live directory instead of guessing a filename.
|
|
4
|
+
|
|
5
|
+
## Tree
|
|
6
|
+
|
|
7
|
+
\`/linear/LAYOUT.md\` is this guide.
|
|
8
|
+
\`/linear/issues/\`, \`/linear/comments/\`, \`/linear/users/\`, and \`/linear/teams/\` each own their canonical JSON records plus a sibling \`_index.json\`.
|
|
9
|
+
Other integration-owned trees include \`/linear/projects/\`, \`/linear/cycles/\`, \`/linear/milestones/\`, and \`/linear/roadmaps/\`.
|
|
10
|
+
|
|
11
|
+
## Indexes
|
|
12
|
+
|
|
13
|
+
\`/linear/issues/_index.json\` rows use:
|
|
14
|
+
|
|
15
|
+
\`\`\`json
|
|
16
|
+
{ "id": "<id>", "title": "<human-readable>", "updated": "<iso8601>", "identifier": "<TEAM-123>", "state": "<state name>" }
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
\`/linear/comments/_index.json\`, \`/linear/users/_index.json\`, and \`/linear/teams/_index.json\` use:
|
|
20
|
+
|
|
21
|
+
\`\`\`json
|
|
22
|
+
{ "id": "<id>", "title": "<human-readable>", "updated": "<iso8601>" }
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
## JSONL And Querying
|
|
26
|
+
|
|
27
|
+
Linear does not emit JSONL in this adapter today. Comments are individual \`.json\` records rather than \`comments.jsonl\`.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
ls /linear/issues
|
|
33
|
+
jq '.[0]' /linear/issues/_index.json
|
|
34
|
+
jq '.[] | {identifier, state, title}' /linear/issues/_index.json
|
|
35
|
+
grep -R "ENG-" /linear/comments
|
|
36
|
+
\`\`\`
|
|
37
|
+
`;
|
|
38
|
+
export function linearLayoutPromptFile() {
|
|
39
|
+
return {
|
|
40
|
+
path: '/linear/LAYOUT.md',
|
|
41
|
+
contentType: 'text/markdown; charset=utf-8',
|
|
42
|
+
content: LINEAR_LAYOUT_PROMPT.endsWith('\n') ? LINEAR_LAYOUT_PROMPT : `${LINEAR_LAYOUT_PROMPT}\n`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=layout-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-prompt.js","sourceRoot":"","sources":["../src/layout-prompt.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCnC,CAAC;AAEF,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,WAAW,EAAE,8BAAuC;QACpD,OAAO,EAAE,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,GAAG,oBAAoB,IAAI;KAClG,CAAC;AACJ,CAAC"}
|
package/dist/linear-adapter.d.ts
CHANGED
|
@@ -42,9 +42,17 @@ export interface DeleteFileInput {
|
|
|
42
42
|
workspaceId: string;
|
|
43
43
|
path: string;
|
|
44
44
|
}
|
|
45
|
+
export interface ReadFileInput {
|
|
46
|
+
workspaceId: string;
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ReadFileResult {
|
|
50
|
+
content?: string;
|
|
51
|
+
}
|
|
45
52
|
export interface RelayFileClientLike {
|
|
46
53
|
writeFile(input: WriteFileInput): Promise<WriteFileResult | void>;
|
|
47
54
|
deleteFile?(input: DeleteFileInput): Promise<void> | void;
|
|
55
|
+
readFile?(inputOrWorkspaceId: ReadFileInput | string, path?: string): Promise<ReadFileResult | string | undefined> | ReadFileResult | string | undefined;
|
|
48
56
|
}
|
|
49
57
|
export declare abstract class IntegrationAdapter {
|
|
50
58
|
protected readonly client: RelayFileClientLike;
|
|
@@ -69,5 +77,8 @@ export declare class LinearAdapter extends IntegrationAdapter {
|
|
|
69
77
|
private normalizeEvent;
|
|
70
78
|
private isRemoveEvent;
|
|
71
79
|
private renderContent;
|
|
80
|
+
private writeAuxiliaryFiles;
|
|
81
|
+
private readIndexRows;
|
|
82
|
+
private recordAuxiliaryWrite;
|
|
72
83
|
}
|
|
73
84
|
//# sourceMappingURL=linear-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear-adapter.d.ts","sourceRoot":"","sources":["../src/linear-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"linear-adapter.d.ts","sourceRoot":"","sources":["../src/linear-adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/linear-adapter.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { computeLinearPath, linearCyclePath, linearIssuePath, linearMilestonePath, linearProjectPath, linearRoadmapPath, linearTeamPath, linearUserPath, normalizeLinearObjectType, } from './path-mapper.js';
|
|
1
|
+
import { computeLinearPath, LINEAR_PATH_ROOT, linearByIdAliasPath, linearByTitleAliasPath, linearCyclePath, linearIssueByStatePath, linearIssuePath, linearMilestonePath, linearProjectPath, linearRoadmapPath, linearTeamPath, linearUserPath, normalizeLinearObjectType, } from './path-mapper.js';
|
|
2
|
+
import { buildLinearIndexFile } from './index-emitter.js';
|
|
3
|
+
import { linearLayoutPromptFile } from './layout-prompt.js';
|
|
4
|
+
import { getLinearCommentHumanReadable, getLinearIssueHumanReadable, linearCommentIndexRow, linearIssueIndexRow, linearTeamIndexRow, linearUserIndexRow, } from './queries.js';
|
|
2
5
|
import { LINEAR_WEBHOOK_OBJECT_TYPES } from './types.js';
|
|
3
6
|
export class IntegrationAdapter {
|
|
4
7
|
client;
|
|
@@ -29,16 +32,35 @@ export class LinearAdapter extends IntegrationAdapter {
|
|
|
29
32
|
async ingestWebhook(workspaceId, event) {
|
|
30
33
|
try {
|
|
31
34
|
const normalized = this.normalizeEvent(event);
|
|
32
|
-
const path = computeLinearPath(normalized.objectType, normalized.objectId,
|
|
35
|
+
const path = computeLinearPath(normalized.objectType, normalized.objectId, readPathHumanReadable(normalized.objectType, normalized.payload));
|
|
36
|
+
const content = this.renderContent(workspaceId, normalized, false);
|
|
37
|
+
const semantics = this.computeSemantics(normalized.objectType, normalized.objectId, normalized.payload);
|
|
38
|
+
const aliasErrorPath = inferIssueStateAliasErrorPath(normalized);
|
|
33
39
|
if (this.isRemoveEvent(normalized)) {
|
|
40
|
+
const deletePaths = [path];
|
|
41
|
+
let filesDeleted = 0;
|
|
42
|
+
let filesWritten = 0;
|
|
43
|
+
let filesUpdated = 0;
|
|
44
|
+
const aliasPath = resolveIssueStateAliasPath(normalized.payload);
|
|
45
|
+
const previousAliasPath = resolvePreviousIssueStateAliasPath(normalized.payload);
|
|
34
46
|
if (this.client.deleteFile) {
|
|
35
47
|
await this.client.deleteFile({ workspaceId, path });
|
|
48
|
+
filesDeleted += 1;
|
|
49
|
+
for (const candidatePath of uniqueStrings([aliasPath, previousAliasPath])) {
|
|
50
|
+
if (!candidatePath) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
await this.client.deleteFile({ workspaceId, path: candidatePath });
|
|
54
|
+
deletePaths.push(candidatePath);
|
|
55
|
+
filesDeleted += 1;
|
|
56
|
+
}
|
|
57
|
+
const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, true);
|
|
36
58
|
return {
|
|
37
|
-
filesWritten:
|
|
38
|
-
filesUpdated:
|
|
39
|
-
filesDeleted
|
|
40
|
-
paths: [
|
|
41
|
-
errors:
|
|
59
|
+
filesWritten: filesWritten + auxiliary.filesWritten,
|
|
60
|
+
filesUpdated: filesUpdated + auxiliary.filesUpdated,
|
|
61
|
+
filesDeleted,
|
|
62
|
+
paths: [...deletePaths, ...auxiliary.paths],
|
|
63
|
+
errors: auxiliary.errors,
|
|
42
64
|
};
|
|
43
65
|
}
|
|
44
66
|
const deleteResult = await this.client.writeFile({
|
|
@@ -46,32 +68,99 @@ export class LinearAdapter extends IntegrationAdapter {
|
|
|
46
68
|
path,
|
|
47
69
|
content: this.renderContent(workspaceId, normalized, true),
|
|
48
70
|
contentType: JSON_CONTENT_TYPE,
|
|
49
|
-
semantics
|
|
71
|
+
semantics,
|
|
50
72
|
});
|
|
51
73
|
const counts = inferWriteCounts(normalized, deleteResult, true);
|
|
74
|
+
filesDeleted += counts.filesDeleted;
|
|
75
|
+
filesWritten += counts.filesWritten;
|
|
76
|
+
filesUpdated += counts.filesUpdated;
|
|
77
|
+
for (const candidatePath of uniqueStrings([aliasPath, previousAliasPath])) {
|
|
78
|
+
if (!candidatePath) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const aliasDeleteResult = await this.client.writeFile({
|
|
82
|
+
workspaceId,
|
|
83
|
+
path: candidatePath,
|
|
84
|
+
content: this.renderContent(workspaceId, normalized, true),
|
|
85
|
+
contentType: JSON_CONTENT_TYPE,
|
|
86
|
+
semantics,
|
|
87
|
+
});
|
|
88
|
+
const aliasCounts = inferWriteCounts(normalized, aliasDeleteResult, true);
|
|
89
|
+
deletePaths.push(candidatePath);
|
|
90
|
+
filesDeleted += aliasCounts.filesDeleted;
|
|
91
|
+
filesWritten += aliasCounts.filesWritten;
|
|
92
|
+
filesUpdated += aliasCounts.filesUpdated;
|
|
93
|
+
}
|
|
94
|
+
const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, true);
|
|
52
95
|
return {
|
|
53
|
-
filesWritten:
|
|
54
|
-
filesUpdated:
|
|
55
|
-
filesDeleted
|
|
56
|
-
paths: [
|
|
57
|
-
errors:
|
|
96
|
+
filesWritten: filesWritten + auxiliary.filesWritten,
|
|
97
|
+
filesUpdated: filesUpdated + auxiliary.filesUpdated,
|
|
98
|
+
filesDeleted,
|
|
99
|
+
paths: [...deletePaths, ...auxiliary.paths],
|
|
100
|
+
errors: auxiliary.errors,
|
|
58
101
|
};
|
|
59
102
|
}
|
|
60
103
|
const writeResult = await this.client.writeFile({
|
|
61
104
|
workspaceId,
|
|
62
105
|
path,
|
|
63
|
-
content
|
|
106
|
+
content,
|
|
64
107
|
contentType: JSON_CONTENT_TYPE,
|
|
65
|
-
semantics
|
|
108
|
+
semantics,
|
|
66
109
|
});
|
|
110
|
+
await writeLinearAliases(this.client, workspaceId, normalized, path, content, semantics);
|
|
67
111
|
const counts = inferWriteCounts(normalized, writeResult, false);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, false);
|
|
113
|
+
const result = {
|
|
114
|
+
filesWritten: counts.filesWritten + auxiliary.filesWritten,
|
|
115
|
+
filesUpdated: counts.filesUpdated + auxiliary.filesUpdated,
|
|
71
116
|
filesDeleted: 0,
|
|
72
|
-
paths: [path],
|
|
73
|
-
errors:
|
|
117
|
+
paths: [path, ...auxiliary.paths],
|
|
118
|
+
errors: auxiliary.errors,
|
|
74
119
|
};
|
|
120
|
+
if (normalized.objectType !== 'issue') {
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
const previousAliasPath = resolvePreviousIssueStateAliasPath(normalized.payload);
|
|
124
|
+
const aliasPath = resolveIssueStateAliasPath(normalized.payload);
|
|
125
|
+
if (previousAliasPath && previousAliasPath !== aliasPath) {
|
|
126
|
+
if (this.client.deleteFile) {
|
|
127
|
+
await this.client.deleteFile({ workspaceId, path: previousAliasPath });
|
|
128
|
+
result.filesDeleted += 1;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const previousDeleteResult = await this.client.writeFile({
|
|
132
|
+
workspaceId,
|
|
133
|
+
path: previousAliasPath,
|
|
134
|
+
content: this.renderContent(workspaceId, normalized, true),
|
|
135
|
+
contentType: JSON_CONTENT_TYPE,
|
|
136
|
+
semantics,
|
|
137
|
+
});
|
|
138
|
+
const previousCounts = inferWriteCounts(normalized, previousDeleteResult, true);
|
|
139
|
+
result.filesWritten += previousCounts.filesWritten;
|
|
140
|
+
result.filesUpdated += previousCounts.filesUpdated;
|
|
141
|
+
result.filesDeleted += previousCounts.filesDeleted;
|
|
142
|
+
}
|
|
143
|
+
result.paths.push(previousAliasPath);
|
|
144
|
+
}
|
|
145
|
+
if (!aliasPath) {
|
|
146
|
+
result.errors.push({
|
|
147
|
+
path: aliasErrorPath,
|
|
148
|
+
error: 'Linear issue is missing state_name or identifier for by-state alias emission.',
|
|
149
|
+
});
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
const aliasWriteResult = await this.client.writeFile({
|
|
153
|
+
workspaceId,
|
|
154
|
+
path: aliasPath,
|
|
155
|
+
content,
|
|
156
|
+
contentType: JSON_CONTENT_TYPE,
|
|
157
|
+
semantics,
|
|
158
|
+
});
|
|
159
|
+
const aliasCounts = inferWriteCounts(normalized, aliasWriteResult, false);
|
|
160
|
+
result.filesWritten += aliasCounts.filesWritten;
|
|
161
|
+
result.filesUpdated += aliasCounts.filesUpdated;
|
|
162
|
+
result.paths.push(aliasPath);
|
|
163
|
+
return result;
|
|
75
164
|
}
|
|
76
165
|
catch (error) {
|
|
77
166
|
const fallbackPath = inferFallbackPath(event);
|
|
@@ -195,6 +284,69 @@ export class LinearAdapter extends IntegrationAdapter {
|
|
|
195
284
|
payload: event.payload,
|
|
196
285
|
});
|
|
197
286
|
}
|
|
287
|
+
async writeAuxiliaryFiles(workspaceId, event, deleted) {
|
|
288
|
+
const result = {
|
|
289
|
+
filesWritten: 0,
|
|
290
|
+
filesUpdated: 0,
|
|
291
|
+
paths: [],
|
|
292
|
+
errors: [],
|
|
293
|
+
};
|
|
294
|
+
const layoutFile = linearLayoutPromptFile();
|
|
295
|
+
await this.recordAuxiliaryWrite(result, workspaceId, layoutFile.path, layoutFile.content, layoutFile.contentType);
|
|
296
|
+
const bucket = bucketForObjectType(event.objectType);
|
|
297
|
+
if (!bucket) {
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
const existingRows = await this.readIndexRows(workspaceId, buildIndexPathForBucket(bucket));
|
|
301
|
+
if (!existingRows) {
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
const nextRows = deleted
|
|
305
|
+
? existingRows.filter((row) => row.id !== event.objectId)
|
|
306
|
+
: upsertLinearIndexRow(existingRows, buildIndexRow(bucket, event));
|
|
307
|
+
const indexFile = buildIndexFileForBucket(bucket, nextRows);
|
|
308
|
+
await this.recordAuxiliaryWrite(result, workspaceId, indexFile.path, indexFile.content, indexFile.contentType);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
async readIndexRows(workspaceId, path) {
|
|
312
|
+
const content = await readClientFile(this.client, workspaceId, path);
|
|
313
|
+
if (content === READ_NOT_AVAILABLE) {
|
|
314
|
+
// No reader on the client — we can't reconcile, so skip the auxiliary write entirely.
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
if (content === undefined) {
|
|
318
|
+
// Reader ran but the index is missing/empty/malformed. Bootstrap with an empty
|
|
319
|
+
// array so the first ingest writes the index instead of getting stuck.
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(content);
|
|
324
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async recordAuxiliaryWrite(result, workspaceId, path, content, contentType) {
|
|
331
|
+
try {
|
|
332
|
+
const writeResult = await this.client.writeFile({
|
|
333
|
+
workspaceId,
|
|
334
|
+
path,
|
|
335
|
+
content,
|
|
336
|
+
contentType,
|
|
337
|
+
});
|
|
338
|
+
const counts = inferAuxiliaryWriteCounts(writeResult);
|
|
339
|
+
result.filesWritten += counts.filesWritten;
|
|
340
|
+
result.filesUpdated += counts.filesUpdated;
|
|
341
|
+
result.paths.push(path);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
result.errors.push({
|
|
345
|
+
path,
|
|
346
|
+
error: toErrorMessage(error),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
198
350
|
}
|
|
199
351
|
function applyIssueSemantics(properties, relations, payload) {
|
|
200
352
|
const issue = payload;
|
|
@@ -267,12 +419,12 @@ function applyIssueSemantics(properties, relations, payload) {
|
|
|
267
419
|
addStringProperty(properties, 'linear.cycle_name', issue.cycle.name);
|
|
268
420
|
}
|
|
269
421
|
if (issue.parent?.id) {
|
|
270
|
-
relations.add(
|
|
422
|
+
relations.add(buildLinearIssueReferencePath(issue.parent));
|
|
271
423
|
addStringProperty(properties, 'linear.parent_id', issue.parent.id);
|
|
272
424
|
}
|
|
273
425
|
for (const child of issue.children ?? []) {
|
|
274
426
|
if (child.id) {
|
|
275
|
-
relations.add(
|
|
427
|
+
relations.add(buildLinearIssueReferencePath(child));
|
|
276
428
|
}
|
|
277
429
|
}
|
|
278
430
|
for (const relation of asRelations(issue.relations)) {
|
|
@@ -294,11 +446,15 @@ function applyIssueSemantics(properties, relations, payload) {
|
|
|
294
446
|
addFirstStringProperty(properties, 'linear.team_key', properties['linear.team_key'], issue.team_key);
|
|
295
447
|
addFirstStringProperty(properties, 'linear.team_name', properties['linear.team_name'], issue.team_name);
|
|
296
448
|
}
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
449
|
+
function readPathHumanReadable(objectType, payload) {
|
|
450
|
+
switch (normalizeLinearObjectType(objectType)) {
|
|
451
|
+
case 'issue':
|
|
452
|
+
return getLinearIssueHumanReadable(buildLinearIssueHumanReadableInput(payload));
|
|
453
|
+
case 'comment':
|
|
454
|
+
return getLinearCommentHumanReadable(buildLinearCommentHumanReadableInput(payload));
|
|
455
|
+
default:
|
|
456
|
+
return undefined;
|
|
300
457
|
}
|
|
301
|
-
return asString(payload.title);
|
|
302
458
|
}
|
|
303
459
|
function applyCommentSemantics(properties, relations, comments, payload) {
|
|
304
460
|
const comment = payload;
|
|
@@ -323,7 +479,7 @@ function applyCommentSemantics(properties, relations, comments, payload) {
|
|
|
323
479
|
addFirstStringProperty(properties, 'linear.author_name', properties['linear.author_name'], comment.user_name, comment.author_name);
|
|
324
480
|
addFirstStringProperty(properties, 'linear.author_email', properties['linear.author_email'], comment.user_email, comment.author_email);
|
|
325
481
|
if (comment.issue?.id) {
|
|
326
|
-
relations.add(
|
|
482
|
+
relations.add(buildLinearIssueReferencePath(comment.issue));
|
|
327
483
|
addStringProperty(properties, 'linear.issue_id', comment.issue.id);
|
|
328
484
|
addStringProperty(properties, 'linear.issue_identifier', comment.issue.identifier);
|
|
329
485
|
addStringProperty(properties, 'linear.issue_title', comment.issue.title);
|
|
@@ -331,7 +487,10 @@ function applyCommentSemantics(properties, relations, comments, payload) {
|
|
|
331
487
|
}
|
|
332
488
|
const issueId = asString(comment.issue_id);
|
|
333
489
|
if (issueId) {
|
|
334
|
-
relations.add(linearIssuePath(issueId
|
|
490
|
+
relations.add(linearIssuePath(issueId, getLinearIssueHumanReadable(buildLinearIssueHumanReadableInput({
|
|
491
|
+
identifier: comment.issue_identifier,
|
|
492
|
+
title: comment.issue_title,
|
|
493
|
+
}))));
|
|
335
494
|
addStringProperty(properties, 'linear.issue_id', issueId);
|
|
336
495
|
}
|
|
337
496
|
addFirstStringProperty(properties, 'linear.issue_identifier', properties['linear.issue_identifier'], comment.issue_identifier);
|
|
@@ -369,6 +528,29 @@ function applyProjectSemantics(properties, relations, payload) {
|
|
|
369
528
|
}
|
|
370
529
|
}
|
|
371
530
|
}
|
|
531
|
+
function buildLinearIssueReferencePath(issue) {
|
|
532
|
+
return linearIssuePath(issue.id, getLinearIssueHumanReadable(issue));
|
|
533
|
+
}
|
|
534
|
+
function buildLinearIssueHumanReadableInput(record) {
|
|
535
|
+
const identifier = asString(record.identifier);
|
|
536
|
+
const title = asString(record.title);
|
|
537
|
+
return {
|
|
538
|
+
...(identifier ? { identifier } : {}),
|
|
539
|
+
...(title ? { title } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function buildLinearCommentIssueInput(issue) {
|
|
543
|
+
const identifier = asString(issue?.identifier);
|
|
544
|
+
return identifier ? { identifier } : undefined;
|
|
545
|
+
}
|
|
546
|
+
function buildLinearCommentHumanReadableInput(record) {
|
|
547
|
+
const body = asString(record.body);
|
|
548
|
+
const issue = buildLinearCommentIssueInput(getRecord(record.issue));
|
|
549
|
+
return {
|
|
550
|
+
...(body ? { body } : {}),
|
|
551
|
+
...(issue ? { issue } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
372
554
|
function applyCycleSemantics(properties, payload) {
|
|
373
555
|
const cycle = payload;
|
|
374
556
|
addNumberProperty(properties, 'linear.number', cycle.number);
|
|
@@ -467,12 +649,90 @@ function mergeLinearPayload(event) {
|
|
|
467
649
|
}),
|
|
468
650
|
};
|
|
469
651
|
}
|
|
652
|
+
function bucketForObjectType(objectType) {
|
|
653
|
+
switch (normalizeLinearObjectType(objectType)) {
|
|
654
|
+
case 'issue':
|
|
655
|
+
return 'issues';
|
|
656
|
+
case 'comment':
|
|
657
|
+
return 'comments';
|
|
658
|
+
case 'team':
|
|
659
|
+
return 'teams';
|
|
660
|
+
case 'user':
|
|
661
|
+
return 'users';
|
|
662
|
+
default:
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function buildIndexRow(bucket, event) {
|
|
667
|
+
const payload = {
|
|
668
|
+
...event.payload,
|
|
669
|
+
id: event.objectId,
|
|
670
|
+
};
|
|
671
|
+
switch (bucket) {
|
|
672
|
+
case 'issues':
|
|
673
|
+
return linearIssueIndexRow(payload);
|
|
674
|
+
case 'comments':
|
|
675
|
+
return linearCommentIndexRow(payload);
|
|
676
|
+
case 'teams':
|
|
677
|
+
return linearTeamIndexRow(payload);
|
|
678
|
+
case 'users':
|
|
679
|
+
return linearUserIndexRow(payload);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function buildIndexFileForBucket(bucket, rows) {
|
|
683
|
+
switch (bucket) {
|
|
684
|
+
case 'issues':
|
|
685
|
+
return buildLinearIndexFile('issues', rows);
|
|
686
|
+
case 'comments':
|
|
687
|
+
return buildLinearIndexFile('comments', rows);
|
|
688
|
+
case 'teams':
|
|
689
|
+
return buildLinearIndexFile('teams', rows);
|
|
690
|
+
case 'users':
|
|
691
|
+
return buildLinearIndexFile('users', rows);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function buildIndexPathForBucket(bucket) {
|
|
695
|
+
return buildIndexFileForBucket(bucket, []).path;
|
|
696
|
+
}
|
|
697
|
+
function upsertLinearIndexRow(rows, row) {
|
|
698
|
+
return [...rows.filter((existing) => existing.id !== row.id), row];
|
|
699
|
+
}
|
|
700
|
+
// `READ_NOT_AVAILABLE` is returned when the client cannot read at all (no
|
|
701
|
+
// `readFile` method). `undefined` means the call ran but the file is missing
|
|
702
|
+
// or the response was malformed. Callers use this distinction to decide
|
|
703
|
+
// whether to skip auxiliary writes (no readFile) or bootstrap an empty index
|
|
704
|
+
// (file missing on first ingest).
|
|
705
|
+
const READ_NOT_AVAILABLE = Symbol('readNotAvailable');
|
|
706
|
+
async function readClientFile(client, workspaceId, path) {
|
|
707
|
+
if (!client.readFile) {
|
|
708
|
+
return READ_NOT_AVAILABLE;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const value = client.readFile.length >= 2
|
|
712
|
+
? await client.readFile(workspaceId, path)
|
|
713
|
+
: await client.readFile({ workspaceId, path });
|
|
714
|
+
if (typeof value === 'string') {
|
|
715
|
+
return value;
|
|
716
|
+
}
|
|
717
|
+
if (value && typeof value === 'object' && typeof value.content === 'string') {
|
|
718
|
+
return value.content;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
function inferAuxiliaryWriteCounts(writeResult) {
|
|
727
|
+
if (writeResult?.created || writeResult?.status === 'created') {
|
|
728
|
+
return { filesWritten: 1, filesUpdated: 0 };
|
|
729
|
+
}
|
|
730
|
+
return { filesWritten: 0, filesUpdated: 1 };
|
|
731
|
+
}
|
|
470
732
|
function inferWriteCounts(event, writeResult, deleted) {
|
|
471
733
|
if (deleted) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
return { filesWritten: 0, filesUpdated: 1, filesDeleted: 0 };
|
|
734
|
+
void writeResult;
|
|
735
|
+
return { filesWritten: 0, filesUpdated: 0, filesDeleted: 1 };
|
|
476
736
|
}
|
|
477
737
|
if (writeResult?.created || writeResult?.status === 'created') {
|
|
478
738
|
return { filesWritten: 1, filesUpdated: 0, filesDeleted: 0 };
|
|
@@ -511,6 +771,33 @@ function inferFallbackPath(event) {
|
|
|
511
771
|
return '';
|
|
512
772
|
}
|
|
513
773
|
}
|
|
774
|
+
function resolveIssueStateAliasPath(payload) {
|
|
775
|
+
const stateName = asString(payload.state_name);
|
|
776
|
+
const identifier = asString(payload.identifier);
|
|
777
|
+
if (!stateName || !identifier) {
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
return linearIssueByStatePath(stateName, identifier);
|
|
781
|
+
}
|
|
782
|
+
function resolvePreviousIssueStateAliasPath(payload) {
|
|
783
|
+
const previousData = getRecord(getRecord(payload._webhook)?.previousData);
|
|
784
|
+
if (!previousData) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
const stateName = asString(previousData.state_name);
|
|
788
|
+
const identifier = asString(previousData.identifier) ?? asString(payload.identifier);
|
|
789
|
+
if (!stateName || !identifier) {
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
792
|
+
return linearIssueByStatePath(stateName, identifier);
|
|
793
|
+
}
|
|
794
|
+
function inferIssueStateAliasErrorPath(event) {
|
|
795
|
+
const identifier = asString(event.payload.identifier);
|
|
796
|
+
if (identifier) {
|
|
797
|
+
return `/linear/issues/by-state/<missing-state>/${encodeURIComponent(identifier)}.json`;
|
|
798
|
+
}
|
|
799
|
+
return '/linear/issues/by-state/<missing-state>/<missing-identifier>.json';
|
|
800
|
+
}
|
|
514
801
|
function extractPayloadId(value) {
|
|
515
802
|
const record = getRecord(value);
|
|
516
803
|
return asString(record?.id);
|
|
@@ -571,6 +858,88 @@ function sortStrings(values) {
|
|
|
571
858
|
function stableJson(value) {
|
|
572
859
|
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
573
860
|
}
|
|
861
|
+
async function writeLinearAliases(client, workspaceId, event, canonicalPath, content, semantics) {
|
|
862
|
+
// duplicate write — the adapter only has a file-write interface, so aliases store the canonical bytes verbatim.
|
|
863
|
+
const normalizedType = normalizeLinearObjectType(event.objectType);
|
|
864
|
+
if (normalizedType !== 'issue' && normalizedType !== 'project') {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const scope = normalizedType === 'issue' ? `${LINEAR_PATH_ROOT}/issues` : `${LINEAR_PATH_ROOT}/projects`;
|
|
868
|
+
const title = normalizedType === 'issue' ? asString(event.payload.title) : asString(event.payload.name);
|
|
869
|
+
const byId = normalizedType === 'issue'
|
|
870
|
+
? asString(event.payload.identifier) ?? event.objectId
|
|
871
|
+
: event.objectId;
|
|
872
|
+
await writeLinearIndex(client, workspaceId, scope);
|
|
873
|
+
await writeLinearFile(client, workspaceId, linearByIdAliasPath(scope, byId), content, semantics);
|
|
874
|
+
if (!title) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const baseAliasPath = linearByTitleAliasPath(scope, title, event.objectId);
|
|
878
|
+
const existingBaseContent = await readLinearFile(client, baseAliasPath);
|
|
879
|
+
const aliasPath = existingBaseContent !== undefined && existingBaseContent !== content
|
|
880
|
+
? linearByTitleAliasPath(scope, title, event.objectId, true)
|
|
881
|
+
: baseAliasPath;
|
|
882
|
+
// TODO(issue #106): remove stale by-title aliases when a record title changes on re-ingest; this wave only writes the current alias.
|
|
883
|
+
await writeLinearFile(client, workspaceId, aliasPath, content, semantics);
|
|
884
|
+
}
|
|
885
|
+
async function writeLinearIndex(client, workspaceId, scope) {
|
|
886
|
+
const indexPath = `${scope}/_index.json`;
|
|
887
|
+
const rows = mergeLinearIndexRows(await readLinearFile(client, indexPath), [
|
|
888
|
+
{ title: 'by-id', file: 'by-id/' },
|
|
889
|
+
{ title: 'by-title', file: 'by-title/' },
|
|
890
|
+
]);
|
|
891
|
+
await writeLinearFile(client, workspaceId, indexPath, stableJson({ rows }), undefined);
|
|
892
|
+
}
|
|
893
|
+
function mergeLinearIndexRows(existingContent, requiredRows) {
|
|
894
|
+
const rows = new Map();
|
|
895
|
+
for (const row of parseLinearIndexRows(existingContent)) {
|
|
896
|
+
rows.set(row.file, row);
|
|
897
|
+
}
|
|
898
|
+
for (const row of requiredRows) {
|
|
899
|
+
rows.set(row.file, row);
|
|
900
|
+
}
|
|
901
|
+
return [...rows.values()].sort((left, right) => left.file.localeCompare(right.file));
|
|
902
|
+
}
|
|
903
|
+
function parseLinearIndexRows(existingContent) {
|
|
904
|
+
if (!existingContent) {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
const parsed = JSON.parse(existingContent);
|
|
909
|
+
return Array.isArray(parsed.rows)
|
|
910
|
+
? parsed.rows.filter((row) => typeof row?.file === 'string' && typeof row?.title === 'string')
|
|
911
|
+
: [];
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function writeLinearFile(client, workspaceId, path, content, semantics) {
|
|
918
|
+
await client.writeFile({
|
|
919
|
+
workspaceId,
|
|
920
|
+
path,
|
|
921
|
+
content,
|
|
922
|
+
contentType: JSON_CONTENT_TYPE,
|
|
923
|
+
...(semantics ? { semantics } : {}),
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function readLinearFile(client, path) {
|
|
927
|
+
const reader = client.readFile;
|
|
928
|
+
if (typeof reader !== 'function') {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const value = await reader.call(client, path);
|
|
933
|
+
return typeof value === 'string'
|
|
934
|
+
? value
|
|
935
|
+
: value && typeof value === 'object' && 'content' in value && typeof value.content === 'string'
|
|
936
|
+
? value.content
|
|
937
|
+
: undefined;
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
574
943
|
function sortJson(value) {
|
|
575
944
|
if (Array.isArray(value)) {
|
|
576
945
|
return value.map(sortJson);
|
|
@@ -628,7 +997,8 @@ function asLinearReferenceIds(value) {
|
|
|
628
997
|
.filter((entry) => entry !== undefined);
|
|
629
998
|
}
|
|
630
999
|
function uniqueStrings(values) {
|
|
631
|
-
return Array.from(new Set(values
|
|
1000
|
+
return Array.from(new Set(values.filter((value) => Boolean(value))))
|
|
1001
|
+
.sort((left, right) => left.localeCompare(right));
|
|
632
1002
|
}
|
|
633
1003
|
function mapPriorityLabel(priority) {
|
|
634
1004
|
switch (priority) {
|