@rawdash/connector-gitlab 0.1.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/dist/index.js ADDED
@@ -0,0 +1,811 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function standardRateLimitPolicy(config) {
8
+ const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;
9
+ const multiplier = resetUnit === "s" ? 1e3 : 1;
10
+ return {
11
+ parse(h) {
12
+ const remainingRaw = h.get(remainingHeader);
13
+ if (remainingRaw === null || remainingRaw.trim() === "") {
14
+ return null;
15
+ }
16
+ const remaining = Number(remainingRaw);
17
+ if (!Number.isFinite(remaining)) {
18
+ return null;
19
+ }
20
+ const resetRaw = h.get(resetHeader);
21
+ if (resetRaw === null) {
22
+ if (resetFallbackMs === void 0) {
23
+ return null;
24
+ }
25
+ return {
26
+ remaining,
27
+ resetAt: new Date(Date.now() + resetFallbackMs)
28
+ };
29
+ }
30
+ if (resetRaw.trim() === "") {
31
+ return null;
32
+ }
33
+ const reset = Number(resetRaw);
34
+ if (!Number.isFinite(reset) || reset < 0) {
35
+ return null;
36
+ }
37
+ const resetMs = reset * multiplier;
38
+ if (!Number.isFinite(resetMs)) {
39
+ return null;
40
+ }
41
+ return { remaining, resetAt: new Date(resetMs) };
42
+ }
43
+ };
44
+ }
45
+ function parseLinkHeader(header) {
46
+ if (!header) {
47
+ return {};
48
+ }
49
+ const result = {};
50
+ for (const part of header.split(",")) {
51
+ const match = part.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/);
52
+ if (match) {
53
+ result[match[2]] = match[1];
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ // src/gitlab.ts
60
+ import {
61
+ BaseConnector,
62
+ defineConfigFields,
63
+ defineConnectorDoc,
64
+ defineResources,
65
+ makeChunkedCursorGuard,
66
+ paginateChunked,
67
+ schemasFromResources,
68
+ selectActivePhases
69
+ } from "@rawdash/core";
70
+ import { z } from "zod";
71
+ var positiveInt = z.number().int().positive();
72
+ var configFields = defineConfigFields(
73
+ z.object({
74
+ apiToken: z.object({ $secret: z.string() }).meta({
75
+ label: "API Token",
76
+ description: "GitLab Personal Access Token with `read_api` scope. Create one at GitLab -> Preferences -> Access Tokens.",
77
+ placeholder: "glpat-...",
78
+ secret: true
79
+ }),
80
+ host: z.string().min(1).regex(
81
+ /^[^/\s:?#]+$/,
82
+ "Use host only (no protocol, port, path, or query)."
83
+ ).optional().meta({
84
+ label: "Host (optional)",
85
+ description: "Your GitLab host. Defaults to `gitlab.com`. For self-hosted, supply the hostname only (e.g. `gitlab.example.com`).",
86
+ placeholder: "gitlab.com"
87
+ }),
88
+ projectIds: z.array(positiveInt).nonempty().optional().meta({
89
+ label: "Project IDs (optional)",
90
+ description: "Numeric project IDs to sync directly (find one in Project -> Settings -> General). Combined with any projects discovered via `groupIds`."
91
+ }),
92
+ groupIds: z.array(positiveInt).nonempty().optional().meta({
93
+ label: "Group IDs (optional)",
94
+ description: "Numeric group IDs whose projects (including subgroups) will be discovered and synced."
95
+ }),
96
+ resources: z.array(
97
+ z.enum([
98
+ "project",
99
+ "merge_request",
100
+ "pipeline",
101
+ "pipeline_event",
102
+ "issue",
103
+ "release"
104
+ ])
105
+ ).nonempty().optional().meta({
106
+ label: "Resources",
107
+ description: "Which GitLab resources to sync. Omit to sync all of them. 'pipeline_event' rides the 'pipeline' phase - enabling it without 'pipeline' still fetches pipelines but skips writing pipeline entities."
108
+ })
109
+ }).refine(
110
+ (v) => v.projectIds && v.projectIds.length > 0 || v.groupIds && v.groupIds.length > 0,
111
+ {
112
+ message: "At least one of `projectIds` or `groupIds` must be provided.",
113
+ path: ["projectIds"]
114
+ }
115
+ )
116
+ );
117
+ var doc = defineConnectorDoc({
118
+ displayName: "GitLab",
119
+ category: "engineering",
120
+ brandColor: "#FC6D26",
121
+ tagline: "Sync projects, merge requests, pipelines, issues, and releases from GitLab.com or a self-hosted GitLab instance.",
122
+ vendor: {
123
+ name: "GitLab",
124
+ apiDocs: "https://docs.gitlab.com/ee/api/",
125
+ website: "https://gitlab.com"
126
+ },
127
+ auth: {
128
+ summary: "A GitLab Personal Access Token (PAT) with the `read_api` scope is required. The PAT must belong to an account with read access to the projects and groups you want to sync. Self-hosted GitLab is supported by overriding the `host` field.",
129
+ setup: [
130
+ "Open GitLab -> User Preferences -> Access Tokens (or the equivalent on your self-hosted instance).",
131
+ "Create a Personal Access Token with the `read_api` scope.",
132
+ 'Store it as a secret and reference it from the connector config as `apiToken: secret("GITLAB_API_TOKEN")`.',
133
+ "Set `projectIds` to a list of numeric project IDs, or `groupIds` to a list of numeric group IDs (or both). At least one must be set.",
134
+ "For self-hosted GitLab, set `host` to your instance hostname (no protocol or path), e.g. `gitlab.example.com`."
135
+ ]
136
+ },
137
+ rateLimit: "GitLab returns standard `RateLimit-Remaining` / `RateLimit-Reset` headers (reset is a Unix timestamp in seconds); list pagination uses the Link header (page size 100).",
138
+ limitations: [
139
+ "Container Registry, Packages, and GitLab Duo / AI features are out of scope.",
140
+ "Pipeline state-transition events are synthesized: one `pipeline_event` is emitted per pipeline lifecycle (created_at to finished_at/updated_at), not one per intermediate state change.",
141
+ "Group project discovery walks each group with `include_subgroups=true`; very large groups may take multiple sync chunks to enumerate."
142
+ ]
143
+ });
144
+ var gitlabCredentials = {
145
+ apiToken: {
146
+ description: "GitLab Personal Access Token",
147
+ auth: "required"
148
+ }
149
+ };
150
+ var DEFAULT_HOST = "gitlab.com";
151
+ var PAGE_SIZE = 100;
152
+ var gitlabRateLimit = standardRateLimitPolicy({
153
+ remainingHeader: "ratelimit-remaining",
154
+ resetHeader: "ratelimit-reset",
155
+ resetUnit: "s"
156
+ });
157
+ var PHASE_ORDER = [
158
+ "projects",
159
+ "merge_requests",
160
+ "pipelines",
161
+ "issues",
162
+ "releases"
163
+ ];
164
+ var isGitLabSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
165
+ function decodePage(page) {
166
+ if (page === null) {
167
+ return { idx: 0, url: null };
168
+ }
169
+ const sep = page.indexOf("|");
170
+ if (sep === -1) {
171
+ return { idx: 0, url: null };
172
+ }
173
+ const idxRaw = Number.parseInt(page.slice(0, sep), 10);
174
+ const url = page.slice(sep + 1);
175
+ return {
176
+ idx: Number.isFinite(idxRaw) && idxRaw >= 0 ? idxRaw : 0,
177
+ url: url === "" ? null : url
178
+ };
179
+ }
180
+ function encodePage(idx, url) {
181
+ return `${idx}|${url ?? ""}`;
182
+ }
183
+ var userRefSchema = z.object({
184
+ id: z.number().int(),
185
+ username: z.string().min(1),
186
+ name: z.string().nullable().optional()
187
+ });
188
+ var projectSchema = z.object({
189
+ id: z.number().int(),
190
+ name: z.string().min(1),
191
+ path_with_namespace: z.string().min(1),
192
+ default_branch: z.string().nullable(),
193
+ web_url: z.string(),
194
+ created_at: z.iso.datetime(),
195
+ last_activity_at: z.iso.datetime().nullable().optional(),
196
+ archived: z.boolean().optional(),
197
+ visibility: z.string().optional()
198
+ });
199
+ var projectsResponseSchema = z.array(projectSchema);
200
+ var mergeRequestSchema = z.object({
201
+ id: z.number().int(),
202
+ iid: z.number().int(),
203
+ project_id: z.number().int(),
204
+ title: z.string(),
205
+ state: z.string().min(1),
206
+ draft: z.boolean().optional(),
207
+ work_in_progress: z.boolean().optional(),
208
+ author: userRefSchema.nullable(),
209
+ assignees: z.array(userRefSchema).optional(),
210
+ source_branch: z.string(),
211
+ target_branch: z.string(),
212
+ created_at: z.iso.datetime(),
213
+ updated_at: z.iso.datetime(),
214
+ merged_at: z.iso.datetime().nullable(),
215
+ closed_at: z.iso.datetime().nullable(),
216
+ web_url: z.string()
217
+ });
218
+ var mergeRequestsResponseSchema = z.array(mergeRequestSchema);
219
+ var pipelineSchema = z.object({
220
+ id: z.number().int(),
221
+ iid: z.number().int().optional(),
222
+ project_id: z.number().int(),
223
+ status: z.string().min(1),
224
+ ref: z.string().nullable(),
225
+ sha: z.string().min(1),
226
+ source: z.string().nullable(),
227
+ created_at: z.iso.datetime(),
228
+ updated_at: z.iso.datetime(),
229
+ started_at: z.iso.datetime().nullable().optional(),
230
+ finished_at: z.iso.datetime().nullable().optional(),
231
+ duration: z.number().nullable().optional(),
232
+ web_url: z.string()
233
+ });
234
+ var pipelinesResponseSchema = z.array(pipelineSchema);
235
+ var issueSchema = z.object({
236
+ id: z.number().int(),
237
+ iid: z.number().int(),
238
+ project_id: z.number().int(),
239
+ title: z.string(),
240
+ state: z.string().min(1),
241
+ labels: z.array(z.string()),
242
+ author: userRefSchema.nullable(),
243
+ assignees: z.array(userRefSchema).optional(),
244
+ created_at: z.iso.datetime(),
245
+ updated_at: z.iso.datetime(),
246
+ closed_at: z.iso.datetime().nullable(),
247
+ web_url: z.string()
248
+ });
249
+ var issuesResponseSchema = z.array(issueSchema);
250
+ var releaseSchema = z.object({
251
+ tag_name: z.string().min(1),
252
+ name: z.string().nullable(),
253
+ description: z.string().nullable().optional(),
254
+ created_at: z.iso.datetime(),
255
+ released_at: z.iso.datetime().nullable(),
256
+ author: userRefSchema.nullable().optional()
257
+ });
258
+ var releasesResponseSchema = z.array(releaseSchema);
259
+ var gitlabResources = defineResources({
260
+ project: {
261
+ shape: "entity",
262
+ description: "GitLab projects (repositories) with namespace path, default branch, and archived/visibility flags.",
263
+ endpoint: "GET /api/v4/projects/{id}",
264
+ notes: "Discovered from configured `projectIds` and from `groupIds` via GET /api/v4/groups/{id}/projects?include_subgroups=true.",
265
+ responses: { projects: projectsResponseSchema }
266
+ },
267
+ merge_request: {
268
+ shape: "entity",
269
+ description: "Open, merged, and closed merge requests with author, source/target branches, and merge timestamps.",
270
+ endpoint: "GET /api/v4/projects/{id}/merge_requests",
271
+ responses: { merge_requests: mergeRequestsResponseSchema }
272
+ },
273
+ pipeline: {
274
+ shape: "entity",
275
+ description: "CI/CD pipelines with status, ref, commit sha, source, duration, and start/finish timestamps.",
276
+ endpoint: "GET /api/v4/projects/{id}/pipelines",
277
+ responses: { pipelines: pipelinesResponseSchema }
278
+ },
279
+ pipeline_event: {
280
+ shape: "event",
281
+ description: "Pipeline lifecycle events. One event per pipeline covering created_at to finished_at (or updated_at if not yet finished), tagged with the terminal status.",
282
+ endpoint: "GET /api/v4/projects/{id}/pipelines",
283
+ notes: "Derived from the same pipelines response that builds the `pipeline` resource; the GitLab API does not expose an intermediate state-transition history endpoint."
284
+ },
285
+ issue: {
286
+ shape: "entity",
287
+ description: "Open and closed issues with labels, author, assignees, and close timestamp.",
288
+ endpoint: "GET /api/v4/projects/{id}/issues",
289
+ responses: { issues: issuesResponseSchema }
290
+ },
291
+ release: {
292
+ shape: "entity",
293
+ description: "Project releases keyed by tag name, including released_at and the publishing author.",
294
+ endpoint: "GET /api/v4/projects/{id}/releases",
295
+ responses: { releases: releasesResponseSchema }
296
+ }
297
+ });
298
+ var id = "gitlab";
299
+ var GitLabConnector = class _GitLabConnector extends BaseConnector {
300
+ static id = id;
301
+ static resources = gitlabResources;
302
+ static schemas = schemasFromResources(gitlabResources);
303
+ static create(input, ctx) {
304
+ const parsed = configFields.parse(input);
305
+ return new _GitLabConnector(
306
+ {
307
+ host: parsed.host ?? DEFAULT_HOST,
308
+ projectIds: parsed.projectIds,
309
+ groupIds: parsed.groupIds,
310
+ resources: parsed.resources
311
+ },
312
+ { apiToken: parsed.apiToken },
313
+ ctx
314
+ );
315
+ }
316
+ id = id;
317
+ credentials = gitlabCredentials;
318
+ effectiveProjectIds = null;
319
+ projectMetadataCache = /* @__PURE__ */ new Map();
320
+ constructor(settings, creds, ctx) {
321
+ super({ ...settings, host: settings.host || DEFAULT_HOST }, creds, ctx);
322
+ }
323
+ buildHeaders() {
324
+ return {
325
+ "PRIVATE-TOKEN": this.creds.apiToken,
326
+ Accept: "application/json",
327
+ "User-Agent": connectorUserAgent("gitlab")
328
+ };
329
+ }
330
+ apiBase() {
331
+ return `https://${this.settings.host}/api/v4`;
332
+ }
333
+ fetch(url, resource, signal) {
334
+ return this.get(url, {
335
+ resource,
336
+ headers: this.buildHeaders(),
337
+ signal,
338
+ rateLimit: gitlabRateLimit
339
+ });
340
+ }
341
+ sanitizeUrl(url, expectedPath) {
342
+ if (!url) {
343
+ return null;
344
+ }
345
+ try {
346
+ const u = new URL(url);
347
+ if (u.protocol !== "https:" || u.host !== this.settings.host) {
348
+ return null;
349
+ }
350
+ if (u.pathname !== expectedPath) {
351
+ return null;
352
+ }
353
+ return u.toString();
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+ static PHASE_RESOURCES = {
359
+ projects: ["project"],
360
+ merge_requests: ["merge_request"],
361
+ pipelines: ["pipeline", "pipeline_event"],
362
+ issues: ["issue"],
363
+ releases: ["release"]
364
+ };
365
+ activePhases(optionsResources) {
366
+ const fromSettings = selectActivePhases(
367
+ (r) => {
368
+ switch (r) {
369
+ case "project":
370
+ return "projects";
371
+ case "merge_request":
372
+ return "merge_requests";
373
+ case "pipeline":
374
+ case "pipeline_event":
375
+ return "pipelines";
376
+ case "issue":
377
+ return "issues";
378
+ case "release":
379
+ return "releases";
380
+ }
381
+ },
382
+ PHASE_ORDER,
383
+ this.settings.resources
384
+ );
385
+ if (optionsResources === void 0) {
386
+ return fromSettings;
387
+ }
388
+ return fromSettings.filter(
389
+ (phase) => _GitLabConnector.PHASE_RESOURCES[phase].some(
390
+ (r) => optionsResources.has(r)
391
+ )
392
+ );
393
+ }
394
+ isResourceAllowed(resource, optionsResources) {
395
+ const fromSettings = this.settings.resources;
396
+ if (fromSettings && fromSettings.length > 0 && !fromSettings.includes(resource)) {
397
+ return false;
398
+ }
399
+ if (optionsResources !== void 0 && !optionsResources.has(resource)) {
400
+ return false;
401
+ }
402
+ return true;
403
+ }
404
+ // -------------------------------------------------------------------------
405
+ // Effective project ID resolution. Combines explicit `projectIds` with all
406
+ // projects under each configured `groupIds` (recursing into subgroups).
407
+ // -------------------------------------------------------------------------
408
+ async resolveEffectiveProjectIds(signal) {
409
+ if (this.effectiveProjectIds !== null) {
410
+ return this.effectiveProjectIds;
411
+ }
412
+ const seen = /* @__PURE__ */ new Set();
413
+ const ordered = [];
414
+ const addId = (n) => {
415
+ if (!seen.has(n)) {
416
+ seen.add(n);
417
+ ordered.push(n);
418
+ }
419
+ };
420
+ for (const pid of this.settings.projectIds ?? []) {
421
+ addId(pid);
422
+ }
423
+ for (const gid of this.settings.groupIds ?? []) {
424
+ const projects = await this.fetchGroupProjects(gid, signal);
425
+ for (const p of projects) {
426
+ this.projectMetadataCache.set(p.id, p);
427
+ addId(p.id);
428
+ }
429
+ }
430
+ ordered.sort((a, b) => a - b);
431
+ this.effectiveProjectIds = ordered;
432
+ return ordered;
433
+ }
434
+ async fetchGroupProjects(groupId, signal) {
435
+ const out = [];
436
+ const baseUrl = `${this.apiBase()}/groups/${groupId}/projects`;
437
+ let url = `${baseUrl}?per_page=${PAGE_SIZE}&include_subgroups=true&archived=false`;
438
+ const expectedPath = `/api/v4/groups/${groupId}/projects`;
439
+ while (url !== null) {
440
+ const res = await this.fetch(
441
+ url,
442
+ "group_projects",
443
+ signal
444
+ );
445
+ for (const project of res.body) {
446
+ out.push(project);
447
+ }
448
+ const next = parseLinkHeader(res.headers.get("link"))["next"] ?? null;
449
+ url = this.sanitizeUrl(next, expectedPath);
450
+ }
451
+ return out;
452
+ }
453
+ // -------------------------------------------------------------------------
454
+ // Per-phase fetchers
455
+ // -------------------------------------------------------------------------
456
+ async fetchProjectMetadata(projectId, signal) {
457
+ if (this.projectMetadataCache.has(projectId)) {
458
+ return this.projectMetadataCache.get(projectId);
459
+ }
460
+ const url = `${this.apiBase()}/projects/${projectId}`;
461
+ const res = await this.fetch(url, "project", signal);
462
+ this.projectMetadataCache.set(projectId, res.body);
463
+ return res.body;
464
+ }
465
+ async fetchProjectsPhase(page, signal) {
466
+ const projects = await this.resolveEffectiveProjectIds(signal);
467
+ if (projects.length === 0) {
468
+ return { items: [], next: null };
469
+ }
470
+ const { idx } = decodePage(page);
471
+ if (idx >= projects.length) {
472
+ return { items: [], next: null };
473
+ }
474
+ const projectId = projects[idx];
475
+ const project = await this.fetchProjectMetadata(projectId, signal);
476
+ const nextIdx = idx + 1;
477
+ const next = nextIdx < projects.length ? encodePage(nextIdx, null) : null;
478
+ return { items: project ? [project] : [], next };
479
+ }
480
+ buildListPageUrl(projectId, resource, options) {
481
+ const u = new URL(`${this.apiBase()}/projects/${projectId}/${resource}`);
482
+ u.searchParams.set("per_page", String(PAGE_SIZE));
483
+ switch (resource) {
484
+ case "merge_requests":
485
+ u.searchParams.set("state", "all");
486
+ u.searchParams.set("order_by", "updated_at");
487
+ u.searchParams.set("sort", "desc");
488
+ u.searchParams.set("scope", "all");
489
+ if (options.since) {
490
+ u.searchParams.set("updated_after", options.since);
491
+ }
492
+ break;
493
+ case "pipelines":
494
+ u.searchParams.set("order_by", "updated_at");
495
+ u.searchParams.set("sort", "desc");
496
+ if (options.since) {
497
+ u.searchParams.set("updated_after", options.since);
498
+ }
499
+ break;
500
+ case "issues":
501
+ u.searchParams.set("state", "all");
502
+ u.searchParams.set("order_by", "updated_at");
503
+ u.searchParams.set("sort", "desc");
504
+ u.searchParams.set("scope", "all");
505
+ if (options.since) {
506
+ u.searchParams.set("updated_after", options.since);
507
+ }
508
+ break;
509
+ case "releases":
510
+ u.searchParams.set("order_by", "released_at");
511
+ u.searchParams.set("sort", "desc");
512
+ break;
513
+ }
514
+ return u.toString();
515
+ }
516
+ async fetchListPhase(options, page, signal, resource, rowUpdatedAt) {
517
+ const projects = await this.resolveEffectiveProjectIds(signal);
518
+ if (projects.length === 0) {
519
+ return { items: [], next: null };
520
+ }
521
+ const { idx, url: rawPageUrl } = decodePage(page);
522
+ if (idx >= projects.length) {
523
+ return { items: [], next: null };
524
+ }
525
+ const projectId = projects[idx];
526
+ const expectedPath = `/api/v4/projects/${projectId}/${resource}`;
527
+ const fetchUrl = this.sanitizeUrl(rawPageUrl, expectedPath) ?? this.buildListPageUrl(projectId, resource, options);
528
+ const res = await this.fetch(fetchUrl, resource, signal);
529
+ const rawNext = parseLinkHeader(res.headers.get("link"))["next"] ?? null;
530
+ const safeNext = this.sanitizeUrl(rawNext, expectedPath);
531
+ const rows = res.body;
532
+ const cutoff = options.since ? new Date(options.since).getTime() : null;
533
+ let filtered;
534
+ let cutoffReached;
535
+ if (cutoff !== null) {
536
+ filtered = rows.filter((row) => rowUpdatedAt(row) >= cutoff);
537
+ const last = rows.at(-1);
538
+ cutoffReached = last !== void 0 && rowUpdatedAt(last) < cutoff;
539
+ } else {
540
+ filtered = rows;
541
+ cutoffReached = false;
542
+ }
543
+ const nextWithinProject = cutoffReached ? null : safeNext;
544
+ const batch = { projectId, items: filtered };
545
+ if (nextWithinProject !== null) {
546
+ return { items: [batch], next: encodePage(idx, nextWithinProject) };
547
+ }
548
+ const nextIdx = idx + 1;
549
+ const next = nextIdx < projects.length ? encodePage(nextIdx, null) : null;
550
+ return { items: [batch], next };
551
+ }
552
+ // -------------------------------------------------------------------------
553
+ // Writers
554
+ // -------------------------------------------------------------------------
555
+ async writeProjects(storage, items, page) {
556
+ if (page === null) {
557
+ await storage.entities([], { types: ["project"] });
558
+ }
559
+ const projects = items;
560
+ for (const project of projects) {
561
+ const updatedAt = new Date(
562
+ project.last_activity_at ?? project.created_at
563
+ ).getTime();
564
+ await storage.entity({
565
+ type: "project",
566
+ id: String(project.id),
567
+ attributes: {
568
+ name: project.name,
569
+ path_with_namespace: project.path_with_namespace,
570
+ default_branch: project.default_branch ?? "",
571
+ web_url: project.web_url,
572
+ visibility: project.visibility ?? "",
573
+ archived: project.archived ?? false,
574
+ created_at: new Date(project.created_at).getTime()
575
+ },
576
+ updated_at: updatedAt
577
+ });
578
+ }
579
+ }
580
+ async writeMergeRequests(storage, items, page, options) {
581
+ if (page === null && !options.since) {
582
+ await storage.entities([], { types: ["merge_request"] });
583
+ }
584
+ const batches = items;
585
+ for (const batch of batches) {
586
+ for (const mr of batch.items) {
587
+ await storage.entity({
588
+ type: "merge_request",
589
+ id: `${batch.projectId}:${mr.iid}`,
590
+ attributes: {
591
+ project_id: batch.projectId,
592
+ iid: mr.iid,
593
+ title: mr.title,
594
+ state: mr.state,
595
+ draft: mr.draft ?? mr.work_in_progress ?? false,
596
+ author: mr.author?.username ?? "",
597
+ assignees: (mr.assignees ?? []).map((a) => a.username),
598
+ source_branch: mr.source_branch,
599
+ target_branch: mr.target_branch,
600
+ web_url: mr.web_url,
601
+ created_at: new Date(mr.created_at).getTime(),
602
+ merged_at: mr.merged_at ? new Date(mr.merged_at).getTime() : null,
603
+ closed_at: mr.closed_at ? new Date(mr.closed_at).getTime() : null
604
+ },
605
+ updated_at: new Date(mr.updated_at).getTime()
606
+ });
607
+ }
608
+ }
609
+ }
610
+ async writePipelines(storage, items, page, options) {
611
+ const pipelineAllowed = this.isResourceAllowed(
612
+ "pipeline",
613
+ options.resources
614
+ );
615
+ const eventAllowed = this.isResourceAllowed(
616
+ "pipeline_event",
617
+ options.resources
618
+ );
619
+ if (page === null && !options.since) {
620
+ if (pipelineAllowed) {
621
+ await storage.entities([], { types: ["pipeline"] });
622
+ }
623
+ if (eventAllowed) {
624
+ await storage.events([], { names: ["pipeline_event"] });
625
+ }
626
+ }
627
+ const batches = items;
628
+ for (const batch of batches) {
629
+ for (const pipeline of batch.items) {
630
+ const createdMs = new Date(pipeline.created_at).getTime();
631
+ const updatedMs = new Date(pipeline.updated_at).getTime();
632
+ const finishedMs = pipeline.finished_at ? new Date(pipeline.finished_at).getTime() : null;
633
+ const durationMs = pipeline.duration !== null && pipeline.duration !== void 0 ? Math.round(pipeline.duration * 1e3) : finishedMs !== null ? finishedMs - createdMs : null;
634
+ if (pipelineAllowed) {
635
+ await storage.entity({
636
+ type: "pipeline",
637
+ id: `${batch.projectId}:${pipeline.id}`,
638
+ attributes: {
639
+ project_id: batch.projectId,
640
+ pipeline_id: pipeline.id,
641
+ status: pipeline.status,
642
+ ref: pipeline.ref ?? "",
643
+ sha: pipeline.sha,
644
+ source: pipeline.source ?? "",
645
+ web_url: pipeline.web_url,
646
+ created_at: createdMs,
647
+ finished_at: finishedMs,
648
+ duration_ms: durationMs
649
+ },
650
+ updated_at: updatedMs
651
+ });
652
+ }
653
+ if (eventAllowed) {
654
+ await storage.event({
655
+ name: "pipeline_event",
656
+ start_ts: createdMs,
657
+ end_ts: finishedMs ?? updatedMs,
658
+ attributes: {
659
+ project_id: batch.projectId,
660
+ pipeline_id: pipeline.id,
661
+ status: pipeline.status,
662
+ ref: pipeline.ref ?? "",
663
+ sha: pipeline.sha,
664
+ source: pipeline.source ?? "",
665
+ duration_ms: durationMs
666
+ }
667
+ });
668
+ }
669
+ }
670
+ }
671
+ }
672
+ async writeIssues(storage, items, page, options) {
673
+ if (page === null && !options.since) {
674
+ await storage.entities([], { types: ["issue"] });
675
+ }
676
+ const batches = items;
677
+ for (const batch of batches) {
678
+ for (const issue of batch.items) {
679
+ await storage.entity({
680
+ type: "issue",
681
+ id: `${batch.projectId}:${issue.iid}`,
682
+ attributes: {
683
+ project_id: batch.projectId,
684
+ iid: issue.iid,
685
+ title: issue.title,
686
+ state: issue.state,
687
+ labels: issue.labels,
688
+ author: issue.author?.username ?? "",
689
+ assignees: (issue.assignees ?? []).map((a) => a.username),
690
+ web_url: issue.web_url,
691
+ created_at: new Date(issue.created_at).getTime(),
692
+ closed_at: issue.closed_at ? new Date(issue.closed_at).getTime() : null
693
+ },
694
+ updated_at: new Date(issue.updated_at).getTime()
695
+ });
696
+ }
697
+ }
698
+ }
699
+ async writeReleases(storage, items, page, options) {
700
+ if (page === null && !options.since) {
701
+ await storage.entities([], { types: ["release"] });
702
+ }
703
+ const batches = items;
704
+ for (const batch of batches) {
705
+ for (const release of batch.items) {
706
+ const createdMs = new Date(release.created_at).getTime();
707
+ const releasedMs = release.released_at ? new Date(release.released_at).getTime() : null;
708
+ await storage.entity({
709
+ type: "release",
710
+ id: `${batch.projectId}:${release.tag_name}`,
711
+ attributes: {
712
+ project_id: batch.projectId,
713
+ tag_name: release.tag_name,
714
+ name: release.name ?? "",
715
+ description: release.description ?? "",
716
+ author: release.author?.username ?? "",
717
+ created_at: createdMs,
718
+ released_at: releasedMs
719
+ },
720
+ updated_at: releasedMs ?? createdMs
721
+ });
722
+ }
723
+ }
724
+ }
725
+ // -------------------------------------------------------------------------
726
+ // Cursor resume
727
+ // -------------------------------------------------------------------------
728
+ resolveCursor(cursor) {
729
+ if (!isGitLabSyncCursor(cursor)) {
730
+ return void 0;
731
+ }
732
+ return { phase: cursor.phase, page: cursor.page };
733
+ }
734
+ // -------------------------------------------------------------------------
735
+ // sync()
736
+ // -------------------------------------------------------------------------
737
+ async sync(options, storage, signal) {
738
+ const cursor = this.resolveCursor(options.cursor);
739
+ const phases = this.activePhases(options.resources);
740
+ return paginateChunked({
741
+ phases,
742
+ cursor,
743
+ signal,
744
+ logger: this.logger,
745
+ fetchPage: async (phase, page, sig) => {
746
+ switch (phase) {
747
+ case "projects":
748
+ return this.fetchProjectsPhase(page, sig);
749
+ case "merge_requests":
750
+ return this.fetchListPhase(
751
+ options,
752
+ page,
753
+ sig,
754
+ "merge_requests",
755
+ (mr) => new Date(mr.updated_at).getTime()
756
+ );
757
+ case "pipelines":
758
+ return this.fetchListPhase(
759
+ options,
760
+ page,
761
+ sig,
762
+ "pipelines",
763
+ (p) => new Date(p.updated_at).getTime()
764
+ );
765
+ case "issues":
766
+ return this.fetchListPhase(
767
+ options,
768
+ page,
769
+ sig,
770
+ "issues",
771
+ (i) => new Date(i.updated_at).getTime()
772
+ );
773
+ case "releases":
774
+ return this.fetchListPhase(
775
+ options,
776
+ page,
777
+ sig,
778
+ "releases",
779
+ (r) => new Date(r.released_at ?? r.created_at).getTime()
780
+ );
781
+ }
782
+ },
783
+ writeBatch: async (phase, items, page) => {
784
+ switch (phase) {
785
+ case "projects":
786
+ return this.writeProjects(storage, items, page);
787
+ case "merge_requests":
788
+ return this.writeMergeRequests(storage, items, page, options);
789
+ case "pipelines":
790
+ return this.writePipelines(storage, items, page, options);
791
+ case "issues":
792
+ return this.writeIssues(storage, items, page, options);
793
+ case "releases":
794
+ return this.writeReleases(storage, items, page, options);
795
+ }
796
+ }
797
+ });
798
+ }
799
+ };
800
+
801
+ // src/index.ts
802
+ var index_default = GitLabConnector;
803
+ export {
804
+ GitLabConnector,
805
+ configFields,
806
+ index_default as default,
807
+ doc,
808
+ id,
809
+ gitlabResources as resources
810
+ };
811
+ //# sourceMappingURL=index.js.map