@rawdash/connector-greenhouse 0.0.1

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,684 @@
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 sanitizeAllowedUrl(options) {
46
+ const { url, host, pathname, protocol = "https:" } = options;
47
+ if (url === null) {
48
+ return null;
49
+ }
50
+ try {
51
+ const u = new URL(url);
52
+ if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {
53
+ return null;
54
+ }
55
+ return u.toString();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ function parseLinkHeader(header) {
61
+ if (!header) {
62
+ return {};
63
+ }
64
+ const result = {};
65
+ for (const part of header.split(",")) {
66
+ const match = part.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/);
67
+ if (match) {
68
+ result[match[2]] = match[1];
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+
74
+ // src/greenhouse.ts
75
+ import {
76
+ BaseConnector,
77
+ defineConfigFields,
78
+ defineConnectorDoc,
79
+ defineResources,
80
+ makeChunkedCursorGuard,
81
+ paginateChunked,
82
+ schemasFromResources,
83
+ selectActivePhases
84
+ } from "@rawdash/core";
85
+ import { z } from "zod";
86
+ var configFields = defineConfigFields(
87
+ z.object({
88
+ apiKey: z.object({ $secret: z.string() }).meta({
89
+ label: "Harvest API key",
90
+ description: "Greenhouse Harvest API key with read-only access. Create one at Configure -> Dev Center -> API Credential Management.",
91
+ placeholder: "ghr_...",
92
+ secret: true
93
+ }),
94
+ resources: z.array(
95
+ z.enum([
96
+ "jobs",
97
+ "candidates",
98
+ "applications",
99
+ "application_events",
100
+ "offers"
101
+ ])
102
+ ).nonempty().optional().meta({
103
+ label: "Resources",
104
+ description: "Which Greenhouse resources to sync. Omit to sync all resources. The Harvest key only needs Get / List permissions for the resources listed here."
105
+ })
106
+ })
107
+ );
108
+ var doc = defineConnectorDoc({
109
+ displayName: "Greenhouse",
110
+ category: "hr",
111
+ brandColor: "#24A47F",
112
+ tagline: "Sync jobs, candidates, applications, and offers from the Greenhouse Harvest API for hiring-funnel, time-to-hire, and offer-rate analytics.",
113
+ vendor: {
114
+ name: "Greenhouse",
115
+ apiDocs: "https://developers.greenhouse.io/harvest.html",
116
+ website: "https://www.greenhouse.com"
117
+ },
118
+ auth: {
119
+ summary: "A Harvest API key with read-only access to candidates, applications, jobs, and offers. Greenhouse authenticates via HTTP Basic with the key as the username and an empty password.",
120
+ setup: [
121
+ "Open Greenhouse -> Configure -> Dev Center -> API Credential Management.",
122
+ "Create a new Harvest API key with Get / List permissions for the resources you intend to sync (Candidates, Applications, Jobs, Offers).",
123
+ "Copy the key once on creation - Greenhouse never shows it again.",
124
+ 'Store the key as a secret and reference it from config as `apiKey: secret("GREENHOUSE_API_KEY")`.'
125
+ ]
126
+ },
127
+ rateLimit: "Greenhouse Harvest enforces 50 requests per 10 seconds per key and surfaces remaining quota via the X-RateLimit-* headers; the shared HTTP client backs off on 429, preferring the Retry-After header.",
128
+ limitations: [
129
+ "Application stage-transition history is derived from each application's built-in timestamps (applied_at, hired_at, rejected_at, last_activity_at) rather than the per-application /activity_feed endpoint, which avoids an N+1 sync.",
130
+ "Greenhouse Onboard data and Recruiting custom fields are out of scope."
131
+ ]
132
+ });
133
+ var cost = {
134
+ recommendedInterval: "1 hour",
135
+ minInterval: "15 minutes",
136
+ warning: "Greenhouse Harvest is rate-limited to 50 requests / 10 seconds per key; on large hiring funnels the full backfill spans many pages, so syncing more often than the recommended interval can starve other integrations on the same key."
137
+ };
138
+ var greenhouseCredentials = {
139
+ apiKey: {
140
+ description: "Greenhouse Harvest API key",
141
+ auth: "required"
142
+ }
143
+ };
144
+ var PHASE_ORDER = [
145
+ "jobs",
146
+ "candidates",
147
+ "applications",
148
+ "application_events",
149
+ "offers"
150
+ ];
151
+ var isGreenhouseSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
152
+ var PER_PAGE = 100;
153
+ var API_HOST = "harvest.greenhouse.io";
154
+ var API_BASE = `https://${API_HOST}`;
155
+ var JOB_ENTITY = "greenhouse_job";
156
+ var CANDIDATE_ENTITY = "greenhouse_candidate";
157
+ var APPLICATION_ENTITY = "greenhouse_application";
158
+ var OFFER_ENTITY = "greenhouse_offer";
159
+ var APPLICATION_EVENT = "greenhouse_application_event";
160
+ var greenhouseRateLimit = standardRateLimitPolicy({
161
+ remainingHeader: "x-ratelimit-remaining",
162
+ resetHeader: "x-ratelimit-reset",
163
+ resetUnit: "s"
164
+ });
165
+ function resourceToPhase(resource) {
166
+ return resource;
167
+ }
168
+ var jobSchema = z.object({
169
+ id: z.number().int(),
170
+ name: z.string(),
171
+ status: z.string().nullish(),
172
+ requisition_id: z.string().nullish(),
173
+ notes: z.string().nullish(),
174
+ confidential: z.boolean().nullish(),
175
+ is_template: z.boolean().nullish(),
176
+ copied_from_id: z.number().nullish(),
177
+ departments: z.array(z.object({ id: z.number().nullish(), name: z.string().nullish() })).nullish(),
178
+ offices: z.array(z.object({ id: z.number().nullish(), name: z.string().nullish() })).nullish(),
179
+ opened_at: z.string().nullish(),
180
+ closed_at: z.string().nullish(),
181
+ created_at: z.string().nullish(),
182
+ updated_at: z.string().nullish()
183
+ });
184
+ var candidateSchema = z.object({
185
+ id: z.number().int(),
186
+ first_name: z.string().nullish(),
187
+ last_name: z.string().nullish(),
188
+ company: z.string().nullish(),
189
+ title: z.string().nullish(),
190
+ is_private: z.boolean().nullish(),
191
+ application_ids: z.array(z.number()).nullish(),
192
+ created_at: z.string().nullish(),
193
+ updated_at: z.string().nullish(),
194
+ last_activity: z.string().nullish()
195
+ });
196
+ var applicationSchema = z.object({
197
+ id: z.number().int(),
198
+ candidate_id: z.number().int(),
199
+ status: z.string().nullish(),
200
+ current_stage: z.object({ id: z.number().nullish(), name: z.string().nullish() }).nullish(),
201
+ applied_at: z.string().nullish(),
202
+ rejected_at: z.string().nullish(),
203
+ last_activity_at: z.string().nullish(),
204
+ source: z.object({ id: z.number().nullish(), public_name: z.string().nullish() }).nullish(),
205
+ jobs: z.array(z.object({ id: z.number().nullish(), name: z.string().nullish() })).nullish()
206
+ });
207
+ var offerSchema = z.object({
208
+ id: z.number().int(),
209
+ application_id: z.number().nullish(),
210
+ candidate_id: z.number().nullish(),
211
+ job_id: z.number().nullish(),
212
+ status: z.string().nullish(),
213
+ created_at: z.string().nullish(),
214
+ sent_at: z.string().nullish(),
215
+ resolved_at: z.string().nullish(),
216
+ starts_at: z.string().nullish()
217
+ });
218
+ var greenhouseResources = defineResources({
219
+ [JOB_ENTITY]: {
220
+ shape: "entity",
221
+ description: "Open, draft, and closed requisitions with department, office, and timestamps for opened / closed transitions.",
222
+ endpoint: "GET /v1/jobs",
223
+ fields: [
224
+ { name: "name", description: "Job title." },
225
+ {
226
+ name: "status",
227
+ description: "Greenhouse job status (open / closed / draft)."
228
+ },
229
+ { name: "requisitionId", description: "External requisition id." },
230
+ {
231
+ name: "departments",
232
+ description: "Flat list of department names attached to the job."
233
+ },
234
+ {
235
+ name: "offices",
236
+ description: "Flat list of office names attached to the job."
237
+ },
238
+ { name: "openedAt", description: "When the job was opened (Unix ms)." },
239
+ { name: "closedAt", description: "When the job was closed (Unix ms)." },
240
+ { name: "confidential", description: "Whether the job is confidential." }
241
+ ],
242
+ responses: { jobs: z.array(jobSchema) }
243
+ },
244
+ [CANDIDATE_ENTITY]: {
245
+ shape: "entity",
246
+ description: "Candidate records with name, title, company, and the count of attached applications.",
247
+ endpoint: "GET /v1/candidates",
248
+ fields: [
249
+ { name: "firstName", description: "Candidate first name." },
250
+ { name: "lastName", description: "Candidate last name." },
251
+ { name: "title", description: "Candidate current job title." },
252
+ { name: "company", description: "Candidate current company." },
253
+ {
254
+ name: "applicationCount",
255
+ description: "Number of applications attached to the candidate. Useful for spotting repeat applicants."
256
+ },
257
+ {
258
+ name: "isPrivate",
259
+ description: "Whether the candidate is marked private."
260
+ },
261
+ {
262
+ name: "createdAt",
263
+ description: "When the candidate was created (Unix ms)."
264
+ },
265
+ {
266
+ name: "lastActivityAt",
267
+ description: "Last activity timestamp on the candidate (Unix ms)."
268
+ }
269
+ ],
270
+ responses: { candidates: z.array(candidateSchema) }
271
+ },
272
+ [APPLICATION_ENTITY]: {
273
+ shape: "entity",
274
+ description: "Applications with status (active / hired / rejected), current stage, source, and the linked candidate / job.",
275
+ endpoint: "GET /v1/applications",
276
+ fields: [
277
+ {
278
+ name: "candidateId",
279
+ description: "Candidate the application belongs to."
280
+ },
281
+ {
282
+ name: "jobId",
283
+ description: "Primary job the application is attached to."
284
+ },
285
+ { name: "jobName", description: "Primary job name at sync time." },
286
+ {
287
+ name: "status",
288
+ description: "Application status (active / hired / rejected)."
289
+ },
290
+ {
291
+ name: "currentStage",
292
+ description: 'Name of the current stage (e.g. "Phone Screen").'
293
+ },
294
+ {
295
+ name: "source",
296
+ description: 'Public source name where the application originated (e.g. "LinkedIn").'
297
+ },
298
+ { name: "appliedAt", description: "When the application was submitted." },
299
+ {
300
+ name: "rejectedAt",
301
+ description: "When the application was rejected (null if not)."
302
+ },
303
+ {
304
+ name: "hiredAt",
305
+ description: "When the application was hired (derived from last_activity_at when status=hired)."
306
+ },
307
+ {
308
+ name: "lastActivityAt",
309
+ description: "Last activity timestamp on the application (Unix ms)."
310
+ }
311
+ ],
312
+ responses: { applications: z.array(applicationSchema) }
313
+ },
314
+ [APPLICATION_EVENT]: {
315
+ shape: "event",
316
+ description: "Application lifecycle events (applied / hired / rejected) derived from each application timestamps. The scope is cleared and rewritten on every sync (including incremental runs).",
317
+ endpoint: "GET /v1/applications",
318
+ notes: "Derived from each application's applied_at / rejected_at / last_activity_at fields, not from a separate API call.",
319
+ fields: [
320
+ {
321
+ name: "applicationId",
322
+ description: "Application the event belongs to."
323
+ },
324
+ { name: "candidateId", description: "Candidate id, denormalised." },
325
+ { name: "jobId", description: "Job id, denormalised." },
326
+ {
327
+ name: "transition",
328
+ description: '"applied", "hired", or "rejected".'
329
+ },
330
+ {
331
+ name: "source",
332
+ description: "Application source name at the time of the event."
333
+ }
334
+ ],
335
+ responses: { application_events: z.array(applicationSchema) }
336
+ },
337
+ [OFFER_ENTITY]: {
338
+ shape: "entity",
339
+ description: "Offers with status (pending / accepted / rejected), linked to their application, candidate, and job.",
340
+ endpoint: "GET /v1/offers",
341
+ fields: [
342
+ { name: "applicationId", description: "Linked application." },
343
+ { name: "candidateId", description: "Linked candidate." },
344
+ { name: "jobId", description: "Linked job." },
345
+ {
346
+ name: "status",
347
+ description: "Offer status (pending / accepted / rejected)."
348
+ },
349
+ { name: "sentAt", description: "When the offer was sent (Unix ms)." },
350
+ {
351
+ name: "resolvedAt",
352
+ description: "When the offer was accepted or rejected (Unix ms; null while pending)."
353
+ },
354
+ {
355
+ name: "startsAt",
356
+ description: "Proposed start date on the offer (Unix ms)."
357
+ }
358
+ ],
359
+ responses: { offers: z.array(offerSchema) }
360
+ }
361
+ });
362
+ function isoToMs(value) {
363
+ if (!value) {
364
+ return null;
365
+ }
366
+ const ms = Date.parse(value);
367
+ return Number.isFinite(ms) ? ms : null;
368
+ }
369
+ function isoToMsOrZero(value) {
370
+ return isoToMs(value) ?? 0;
371
+ }
372
+ function listNames(items) {
373
+ if (!items) {
374
+ return [];
375
+ }
376
+ const names = [];
377
+ for (const item of items) {
378
+ if (item && typeof item.name === "string" && item.name !== "") {
379
+ names.push(item.name);
380
+ }
381
+ }
382
+ return names;
383
+ }
384
+ function primaryJobId(app) {
385
+ const first = app.jobs?.[0];
386
+ if (!first || first.id === null || first.id === void 0) {
387
+ return null;
388
+ }
389
+ return first.id;
390
+ }
391
+ function primaryJobName(app) {
392
+ return app.jobs?.[0]?.name ?? null;
393
+ }
394
+ function hiredAtMs(app) {
395
+ if (app.status !== "hired") {
396
+ return null;
397
+ }
398
+ return isoToMs(app.last_activity_at);
399
+ }
400
+ var id = "greenhouse";
401
+ var GreenhouseConnector = class _GreenhouseConnector extends BaseConnector {
402
+ static id = id;
403
+ static resources = greenhouseResources;
404
+ static schemas = schemasFromResources(greenhouseResources);
405
+ static cost = cost;
406
+ static create(input, ctx) {
407
+ const parsed = configFields.parse(input);
408
+ return new _GreenhouseConnector(
409
+ { resources: parsed.resources },
410
+ { apiKey: parsed.apiKey },
411
+ ctx
412
+ );
413
+ }
414
+ id = id;
415
+ credentials = greenhouseCredentials;
416
+ buildHeaders() {
417
+ const basic = btoa(`${this.creds.apiKey}:`);
418
+ return {
419
+ Authorization: `Basic ${basic}`,
420
+ Accept: "application/json",
421
+ "User-Agent": connectorUserAgent("greenhouse")
422
+ };
423
+ }
424
+ apiGet(url, resource, signal) {
425
+ return this.get(url, {
426
+ resource,
427
+ headers: this.buildHeaders(),
428
+ signal,
429
+ rateLimit: greenhouseRateLimit
430
+ });
431
+ }
432
+ allowedPagePath(phase) {
433
+ switch (phase) {
434
+ case "jobs":
435
+ return "/v1/jobs";
436
+ case "candidates":
437
+ return "/v1/candidates";
438
+ case "applications":
439
+ case "application_events":
440
+ return "/v1/applications";
441
+ case "offers":
442
+ return "/v1/offers";
443
+ }
444
+ }
445
+ sanitizePageUrl(phase, pageUrl) {
446
+ return sanitizeAllowedUrl({
447
+ url: pageUrl,
448
+ host: API_HOST,
449
+ pathname: this.allowedPagePath(phase)
450
+ });
451
+ }
452
+ resolveCursor(cursor) {
453
+ if (!isGreenhouseSyncCursor(cursor)) {
454
+ return void 0;
455
+ }
456
+ if (cursor.phase === "application_events") {
457
+ return { phase: cursor.phase, page: null };
458
+ }
459
+ return {
460
+ phase: cursor.phase,
461
+ page: this.sanitizePageUrl(cursor.phase, cursor.page)
462
+ };
463
+ }
464
+ // Build the initial-page URL for a phase. Subsequent pages come from the
465
+ // Link header, validated through sanitizeAllowedUrl above.
466
+ buildInitialUrl(phase, options) {
467
+ const url = new URL(`${API_BASE}${this.allowedPagePath(phase)}`);
468
+ url.searchParams.set("per_page", String(PER_PAGE));
469
+ if (phase !== "application_events" && options.since) {
470
+ url.searchParams.set("updated_after", options.since);
471
+ }
472
+ return url.toString();
473
+ }
474
+ // -------------------------------------------------------------------------
475
+ // Page fetchers
476
+ // -------------------------------------------------------------------------
477
+ async fetchPhasePage(phase, page, options, signal) {
478
+ const resource = phase === "application_events" ? "application_events" : phase;
479
+ const url = page ?? this.buildInitialUrl(phase, options);
480
+ const res = await this.apiGet(url, resource, signal);
481
+ const rawNext = parseLinkHeader(res.headers.get("link"))["next"] ?? null;
482
+ const next = this.sanitizePageUrl(phase, rawNext);
483
+ return { items: res.body ?? [], next };
484
+ }
485
+ // -------------------------------------------------------------------------
486
+ // Writers
487
+ // -------------------------------------------------------------------------
488
+ async writeJobs(storage, items) {
489
+ for (const job of items) {
490
+ await storage.entity({
491
+ type: JOB_ENTITY,
492
+ id: String(job.id),
493
+ attributes: {
494
+ name: job.name,
495
+ status: job.status ?? null,
496
+ requisitionId: job.requisition_id ?? null,
497
+ confidential: job.confidential ?? null,
498
+ departments: listNames(job.departments),
499
+ offices: listNames(job.offices),
500
+ openedAt: isoToMs(job.opened_at),
501
+ closedAt: isoToMs(job.closed_at),
502
+ createdAt: isoToMs(job.created_at)
503
+ },
504
+ updated_at: isoToMsOrZero(job.updated_at ?? job.created_at)
505
+ });
506
+ }
507
+ }
508
+ async writeCandidates(storage, items) {
509
+ for (const cand of items) {
510
+ await storage.entity({
511
+ type: CANDIDATE_ENTITY,
512
+ id: String(cand.id),
513
+ attributes: {
514
+ firstName: cand.first_name ?? null,
515
+ lastName: cand.last_name ?? null,
516
+ title: cand.title ?? null,
517
+ company: cand.company ?? null,
518
+ applicationCount: cand.application_ids?.length ?? 0,
519
+ isPrivate: cand.is_private ?? null,
520
+ createdAt: isoToMs(cand.created_at),
521
+ lastActivityAt: isoToMs(cand.last_activity)
522
+ },
523
+ updated_at: isoToMsOrZero(
524
+ cand.updated_at ?? cand.last_activity ?? cand.created_at
525
+ )
526
+ });
527
+ }
528
+ }
529
+ async writeApplications(storage, items) {
530
+ for (const app of items) {
531
+ const attributes = {
532
+ candidateId: String(app.candidate_id),
533
+ jobId: primaryJobId(app) === null ? null : String(primaryJobId(app)),
534
+ jobName: primaryJobName(app),
535
+ status: app.status ?? null,
536
+ currentStage: app.current_stage?.name ?? null,
537
+ source: app.source?.public_name ?? null,
538
+ appliedAt: isoToMs(app.applied_at),
539
+ rejectedAt: isoToMs(app.rejected_at),
540
+ hiredAt: hiredAtMs(app),
541
+ lastActivityAt: isoToMs(app.last_activity_at)
542
+ };
543
+ await storage.entity({
544
+ type: APPLICATION_ENTITY,
545
+ id: String(app.id),
546
+ attributes,
547
+ updated_at: isoToMsOrZero(
548
+ app.last_activity_at ?? app.applied_at ?? app.rejected_at
549
+ )
550
+ });
551
+ }
552
+ }
553
+ async writeApplicationEvents(storage, items) {
554
+ for (const app of items) {
555
+ const base = {
556
+ applicationId: String(app.id),
557
+ candidateId: String(app.candidate_id),
558
+ jobId: primaryJobId(app) === null ? null : String(primaryJobId(app)),
559
+ source: app.source?.public_name ?? null
560
+ };
561
+ const appliedMs = isoToMs(app.applied_at);
562
+ if (appliedMs !== null) {
563
+ await storage.event({
564
+ name: APPLICATION_EVENT,
565
+ start_ts: appliedMs,
566
+ end_ts: null,
567
+ attributes: { ...base, transition: "applied" }
568
+ });
569
+ }
570
+ const rejectedMs = isoToMs(app.rejected_at);
571
+ if (rejectedMs !== null) {
572
+ await storage.event({
573
+ name: APPLICATION_EVENT,
574
+ start_ts: rejectedMs,
575
+ end_ts: null,
576
+ attributes: { ...base, transition: "rejected" }
577
+ });
578
+ }
579
+ const hiredMs = hiredAtMs(app);
580
+ if (hiredMs !== null) {
581
+ await storage.event({
582
+ name: APPLICATION_EVENT,
583
+ start_ts: hiredMs,
584
+ end_ts: null,
585
+ attributes: { ...base, transition: "hired" }
586
+ });
587
+ }
588
+ }
589
+ }
590
+ async writeOffers(storage, items) {
591
+ for (const offer of items) {
592
+ await storage.entity({
593
+ type: OFFER_ENTITY,
594
+ id: String(offer.id),
595
+ attributes: {
596
+ applicationId: offer.application_id === null || offer.application_id === void 0 ? null : String(offer.application_id),
597
+ candidateId: offer.candidate_id === null || offer.candidate_id === void 0 ? null : String(offer.candidate_id),
598
+ jobId: offer.job_id === null || offer.job_id === void 0 ? null : String(offer.job_id),
599
+ status: offer.status ?? null,
600
+ sentAt: isoToMs(offer.sent_at),
601
+ resolvedAt: isoToMs(offer.resolved_at),
602
+ startsAt: isoToMs(offer.starts_at)
603
+ },
604
+ updated_at: isoToMsOrZero(
605
+ offer.resolved_at ?? offer.sent_at ?? offer.created_at
606
+ )
607
+ });
608
+ }
609
+ }
610
+ // -------------------------------------------------------------------------
611
+ // Scope clearing
612
+ // -------------------------------------------------------------------------
613
+ async clearScopeOnFirstPage(storage, phase, isFull) {
614
+ if (phase === "application_events") {
615
+ await storage.events([], { names: [APPLICATION_EVENT] });
616
+ return;
617
+ }
618
+ if (!isFull) {
619
+ return;
620
+ }
621
+ const entityType = ENTITY_TYPE_BY_PHASE[phase];
622
+ if (entityType) {
623
+ await storage.entities([], { types: [entityType] });
624
+ }
625
+ }
626
+ async writePhase(storage, phase, items) {
627
+ switch (phase) {
628
+ case "jobs":
629
+ return this.writeJobs(storage, items);
630
+ case "candidates":
631
+ return this.writeCandidates(storage, items);
632
+ case "applications":
633
+ return this.writeApplications(storage, items);
634
+ case "application_events":
635
+ return this.writeApplicationEvents(
636
+ storage,
637
+ items
638
+ );
639
+ case "offers":
640
+ return this.writeOffers(storage, items);
641
+ }
642
+ }
643
+ async sync(options, storage, signal) {
644
+ const cursor = this.resolveCursor(options.cursor);
645
+ const isFull = options.mode === "full";
646
+ const phases = selectActivePhases(
647
+ resourceToPhase,
648
+ PHASE_ORDER,
649
+ this.settings.resources
650
+ );
651
+ return paginateChunked({
652
+ phases,
653
+ cursor,
654
+ signal,
655
+ logger: this.logger,
656
+ fetchPage: async (phase, page, sig) => this.fetchPhasePage(phase, page, options, sig),
657
+ writeBatch: async (phase, items, page) => {
658
+ if (page === null) {
659
+ await this.clearScopeOnFirstPage(storage, phase, isFull);
660
+ }
661
+ await this.writePhase(storage, phase, items);
662
+ }
663
+ });
664
+ }
665
+ };
666
+ var ENTITY_TYPE_BY_PHASE = {
667
+ jobs: JOB_ENTITY,
668
+ candidates: CANDIDATE_ENTITY,
669
+ applications: APPLICATION_ENTITY,
670
+ offers: OFFER_ENTITY
671
+ };
672
+
673
+ // src/index.ts
674
+ var index_default = GreenhouseConnector;
675
+ export {
676
+ GreenhouseConnector,
677
+ configFields,
678
+ cost,
679
+ index_default as default,
680
+ doc,
681
+ id,
682
+ greenhouseResources as resources
683
+ };
684
+ //# sourceMappingURL=index.js.map