@joymerrevent/porters-connect 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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +327 -0
- package/dist/index.d.ts +684 -0
- package/dist/index.js +1255 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
// src/errors/porters-error.ts
|
|
2
|
+
var PortersError = class extends Error {
|
|
3
|
+
category;
|
|
4
|
+
code;
|
|
5
|
+
retryable;
|
|
6
|
+
hint;
|
|
7
|
+
httpStatus;
|
|
8
|
+
context;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(
|
|
11
|
+
message,
|
|
12
|
+
options.cause === void 0 ? void 0 : { cause: options.cause }
|
|
13
|
+
);
|
|
14
|
+
this.name = new.target.name;
|
|
15
|
+
this.category = options.category;
|
|
16
|
+
this.code = options.code ?? null;
|
|
17
|
+
this.retryable = options.retryable ?? false;
|
|
18
|
+
this.hint = options.hint;
|
|
19
|
+
this.httpStatus = options.httpStatus;
|
|
20
|
+
this.context = options.context;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var PortersAuthError = class extends PortersError {
|
|
24
|
+
};
|
|
25
|
+
var PortersResourceError = class extends PortersError {
|
|
26
|
+
};
|
|
27
|
+
var PortersNetworkError = class extends PortersError {
|
|
28
|
+
};
|
|
29
|
+
var PortersConfigError = class extends PortersError {
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/errors/classify.ts
|
|
33
|
+
var RESOURCE_VALIDATION = /* @__PURE__ */ new Set([8, 124, 126, 127, 133, 146, 500]);
|
|
34
|
+
var resourceCategory = (code) => {
|
|
35
|
+
if (code === 9 || code === 302) return "transient";
|
|
36
|
+
if (code === 401 || code === 402) return "auth";
|
|
37
|
+
if (code === 6 || code === 400 || code === 403 || code === 406 || code === 601)
|
|
38
|
+
return "permission";
|
|
39
|
+
if (code === 7 || code === 404) return "notFound";
|
|
40
|
+
if (code === 301 || code === 303 || code === 304) return "conflict";
|
|
41
|
+
if (code === 1e3) return "server";
|
|
42
|
+
if (RESOURCE_VALIDATION.has(code) || code >= 100 && code <= 116)
|
|
43
|
+
return "validation";
|
|
44
|
+
return "unknown";
|
|
45
|
+
};
|
|
46
|
+
var authCategory = (code) => {
|
|
47
|
+
if (code === 400) return "auth";
|
|
48
|
+
if ([401, 103, 104, 105, 106, 107, 109, 114, 117].includes(code))
|
|
49
|
+
return "auth";
|
|
50
|
+
if ([100, 101, 102, 110, 112].includes(code)) return "validation";
|
|
51
|
+
if ([111, 115, 116, 402].includes(code)) return "permission";
|
|
52
|
+
if (code === 108) return "server";
|
|
53
|
+
return "unknown";
|
|
54
|
+
};
|
|
55
|
+
var resourceHint = (code) => {
|
|
56
|
+
if (code === 403)
|
|
57
|
+
return "No data permission. Run the initial browser `code` grant for this Company DB, or check scopes.";
|
|
58
|
+
if (code === 404)
|
|
59
|
+
return "Partition not found or outside the contract period. Verify the partition id.";
|
|
60
|
+
return void 0;
|
|
61
|
+
};
|
|
62
|
+
var resourceError = (code, message, context) => {
|
|
63
|
+
const category = resourceCategory(code);
|
|
64
|
+
return new PortersResourceError(message, {
|
|
65
|
+
category,
|
|
66
|
+
code,
|
|
67
|
+
retryable: category === "transient",
|
|
68
|
+
hint: resourceHint(code),
|
|
69
|
+
context
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
var authError = (code, message) => {
|
|
73
|
+
const category = authCategory(code);
|
|
74
|
+
return new PortersAuthError(message, {
|
|
75
|
+
category,
|
|
76
|
+
code,
|
|
77
|
+
retryable: false,
|
|
78
|
+
hint: category === "auth" ? "Authentication failed; re-authenticate (browser `code` grant) or check app credentials." : void 0
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/xml/parser.ts
|
|
83
|
+
import { XMLParser } from "fast-xml-parser";
|
|
84
|
+
|
|
85
|
+
// src/xml/raw.ts
|
|
86
|
+
var asRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
87
|
+
var asString = (v) => typeof v === "string" ? v : void 0;
|
|
88
|
+
var asArray = (v) => {
|
|
89
|
+
if (Array.isArray(v)) return v;
|
|
90
|
+
return v === void 0 || v === null ? [] : [v];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/xml/parser.ts
|
|
94
|
+
var parser = new XMLParser({
|
|
95
|
+
ignoreAttributes: false,
|
|
96
|
+
attributeNamePrefix: "@_",
|
|
97
|
+
ignoreDeclaration: true,
|
|
98
|
+
parseTagValue: false,
|
|
99
|
+
parseAttributeValue: false,
|
|
100
|
+
trimValues: true,
|
|
101
|
+
// asArray() already normalizes a single/missing/repeated <Item> into an array,
|
|
102
|
+
// so isArray is belt-and-suspenders: these mutants are equivalent (no test can
|
|
103
|
+
// observe the difference once asArray() runs).
|
|
104
|
+
// Stryker disable next-line ArrowFunction,ConditionalExpression,StringLiteral: equivalent — asArray() normalizes regardless
|
|
105
|
+
isArray: (name) => name === "Item"
|
|
106
|
+
});
|
|
107
|
+
var toInt = (v) => {
|
|
108
|
+
const s = asString(v);
|
|
109
|
+
return s === void 0 ? 0 : Number(s);
|
|
110
|
+
};
|
|
111
|
+
var parseResourcePage = (xml) => {
|
|
112
|
+
const root = asRecord(parser.parse(xml));
|
|
113
|
+
const rootKey = root ? Object.keys(root)[0] : void 0;
|
|
114
|
+
const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
|
|
115
|
+
if (!body) {
|
|
116
|
+
throw new PortersResourceError("unparseable resource response", {
|
|
117
|
+
category: "unknown"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const code = toInt(body.Code);
|
|
121
|
+
if (code !== 0) {
|
|
122
|
+
throw resourceError(code, `resource returned code ${code}`, {
|
|
123
|
+
resource: rootKey
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
total: toInt(body["@_Total"]),
|
|
128
|
+
count: toInt(body["@_Count"]),
|
|
129
|
+
start: toInt(body["@_Start"]),
|
|
130
|
+
items: asArray(body.Item).map((it) => asRecord(it) ?? {})
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
var parseWriteResult = (xml) => {
|
|
134
|
+
const root = asRecord(parser.parse(xml));
|
|
135
|
+
const rootKey = root ? Object.keys(root)[0] : void 0;
|
|
136
|
+
const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
|
|
137
|
+
if (!body) {
|
|
138
|
+
throw new PortersResourceError("unparseable write response", {
|
|
139
|
+
category: "unknown"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return asArray(body.Item).map((it) => {
|
|
143
|
+
const item = asRecord(it) ?? {};
|
|
144
|
+
return { id: toInt(item.Id), code: toInt(item.Code) };
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
var parseAuthentication = (xml) => {
|
|
148
|
+
const root = asRecord(parser.parse(xml));
|
|
149
|
+
const body = root ? asRecord(root.Authentication) : void 0;
|
|
150
|
+
if (!body) {
|
|
151
|
+
throw new PortersAuthError("unparseable authentication response", {
|
|
152
|
+
category: "unknown"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const error = toInt(body.Error);
|
|
156
|
+
if (error !== 0) {
|
|
157
|
+
throw authError(
|
|
158
|
+
error,
|
|
159
|
+
asString(body.Message) ?? `authentication error ${error}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const num = (v) => {
|
|
163
|
+
const s = asString(v);
|
|
164
|
+
return s === void 0 ? void 0 : Number(s);
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
code: asString(body.Code),
|
|
168
|
+
accessToken: asString(body.AccessToken),
|
|
169
|
+
accessTokenExpiresIn: num(body.AccessTokenExpiresIn),
|
|
170
|
+
refreshToken: asString(body.RefreshToken),
|
|
171
|
+
refreshTokenExpiresIn: num(body.RefreshTokenExpiresIn)
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/auth/memory-store.ts
|
|
176
|
+
var createMemoryTokenStore = () => {
|
|
177
|
+
let tokens;
|
|
178
|
+
return {
|
|
179
|
+
get: () => Promise.resolve(tokens),
|
|
180
|
+
set: (t) => {
|
|
181
|
+
tokens = t;
|
|
182
|
+
return Promise.resolve();
|
|
183
|
+
},
|
|
184
|
+
clear: () => {
|
|
185
|
+
tokens = void 0;
|
|
186
|
+
return Promise.resolve();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/auth/token-provider.ts
|
|
192
|
+
var DEFAULT_MARGIN_MS = 6e4;
|
|
193
|
+
var createDefaultTokenProvider = (opts) => {
|
|
194
|
+
const store = opts.tokenStore ?? createMemoryTokenStore();
|
|
195
|
+
const margin = opts.refreshMarginMs ?? DEFAULT_MARGIN_MS;
|
|
196
|
+
const now = opts.now ?? (() => Date.now());
|
|
197
|
+
let cached;
|
|
198
|
+
let inflight;
|
|
199
|
+
const accessValid = (t) => t !== void 0 && now() < t.accessTokenExpiresAt - margin;
|
|
200
|
+
const canRefresh = (t) => t !== void 0 && now() < t.refreshTokenExpiresAt - margin;
|
|
201
|
+
const exchange = async (grantType, code) => {
|
|
202
|
+
const body = new URLSearchParams({
|
|
203
|
+
app_id: opts.appId,
|
|
204
|
+
secret: opts.appSecret,
|
|
205
|
+
grant_type: grantType,
|
|
206
|
+
code
|
|
207
|
+
}).toString();
|
|
208
|
+
const res = await opts.transport.send({
|
|
209
|
+
method: "POST",
|
|
210
|
+
url: `https://${opts.host}/v1/token`,
|
|
211
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
212
|
+
body
|
|
213
|
+
});
|
|
214
|
+
const a = parseAuthentication(res.body);
|
|
215
|
+
if (a.accessToken === void 0 || a.refreshToken === void 0) {
|
|
216
|
+
throw new PortersAuthError("token response missing tokens", {
|
|
217
|
+
category: "auth"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const t = {
|
|
221
|
+
accessToken: a.accessToken,
|
|
222
|
+
refreshToken: a.refreshToken,
|
|
223
|
+
accessTokenExpiresAt: now() + (a.accessTokenExpiresIn ?? 0),
|
|
224
|
+
refreshTokenExpiresAt: now() + (a.refreshTokenExpiresIn ?? 0)
|
|
225
|
+
};
|
|
226
|
+
cached = t;
|
|
227
|
+
await store.set(t);
|
|
228
|
+
return t;
|
|
229
|
+
};
|
|
230
|
+
const acquire = async () => {
|
|
231
|
+
const url = `https://${opts.host}/v1/oauth?app_id=${encodeURIComponent(opts.appId)}&response_type=code_direct`;
|
|
232
|
+
const res = await opts.transport.send({ method: "GET", url, headers: {} });
|
|
233
|
+
const { code } = parseAuthentication(res.body);
|
|
234
|
+
if (code === void 0) {
|
|
235
|
+
throw new PortersAuthError("code_direct returned no code", {
|
|
236
|
+
category: "auth"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return exchange("oauth_code", code);
|
|
240
|
+
};
|
|
241
|
+
const renew = async () => {
|
|
242
|
+
if (cached === void 0) cached = await store.get();
|
|
243
|
+
if (canRefresh(cached)) {
|
|
244
|
+
return exchange("refresh_token", cached.refreshToken);
|
|
245
|
+
}
|
|
246
|
+
return acquire();
|
|
247
|
+
};
|
|
248
|
+
const ensure = (forceRefresh) => {
|
|
249
|
+
if (!forceRefresh && accessValid(cached)) return Promise.resolve(cached);
|
|
250
|
+
return inflight ??= renew().finally(() => {
|
|
251
|
+
inflight = void 0;
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
getAccessToken: async (o) => (await ensure(o?.forceRefresh ?? false)).accessToken
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/http/fetch-transport.ts
|
|
260
|
+
var createFetchTransport = (opts = {}) => {
|
|
261
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
262
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
263
|
+
return {
|
|
264
|
+
send: async (req) => {
|
|
265
|
+
try {
|
|
266
|
+
const res = await doFetch(req.url, {
|
|
267
|
+
method: req.method,
|
|
268
|
+
headers: req.headers,
|
|
269
|
+
body: req.body,
|
|
270
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
271
|
+
});
|
|
272
|
+
return { status: res.status, body: await res.text() };
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
throw new PortersNetworkError("transport request failed", {
|
|
275
|
+
category: "network",
|
|
276
|
+
retryable: true,
|
|
277
|
+
cause
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/http/mock-transport.ts
|
|
285
|
+
var AUTH_CODE_XML = `<Authentication><Code>mock-code</Code><Error>0</Error></Authentication>`;
|
|
286
|
+
var AUTH_TOKEN_XML = `<Authentication><AccessToken>mock-access-token</AccessToken><AccessTokenExpiresIn>1800000</AccessTokenExpiresIn><RefreshToken>mock-refresh-token</RefreshToken><RefreshTokenExpiresIn>7200000</RefreshTokenExpiresIn><Error>0</Error></Authentication>`;
|
|
287
|
+
var toResponse = (reply) => typeof reply === "string" ? { status: 200, body: reply } : { status: reply.status ?? 200, body: reply.body };
|
|
288
|
+
var routeLabel = (method, url) => {
|
|
289
|
+
try {
|
|
290
|
+
return `${method} ${new URL(url).pathname}`;
|
|
291
|
+
} catch {
|
|
292
|
+
return `${method} ${url}`;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var createMockTransport = (handler, options = {}) => {
|
|
296
|
+
const autoAuth = options.auth ?? true;
|
|
297
|
+
return {
|
|
298
|
+
send: (req) => {
|
|
299
|
+
if (autoAuth && req.url.includes("/v1/token")) {
|
|
300
|
+
return Promise.resolve({ status: 200, body: AUTH_TOKEN_XML });
|
|
301
|
+
}
|
|
302
|
+
if (autoAuth && req.url.includes("/v1/oauth")) {
|
|
303
|
+
return Promise.resolve({ status: 200, body: AUTH_CODE_XML });
|
|
304
|
+
}
|
|
305
|
+
const reply = handler(req);
|
|
306
|
+
if (reply === void 0) {
|
|
307
|
+
return Promise.reject(
|
|
308
|
+
new PortersConfigError(
|
|
309
|
+
`createMockTransport: no mock response for ${routeLabel(req.method, req.url)} \u2014 add a case to your handler (or pass { auth: false } to mock the auth endpoints too)`,
|
|
310
|
+
{ category: "config" }
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return Promise.resolve(toResponse(reply));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/http/throttle.ts
|
|
320
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
321
|
+
var makeBucket = (capacity, now) => {
|
|
322
|
+
const ratePerMs = capacity / 6e4;
|
|
323
|
+
let tokens = capacity;
|
|
324
|
+
let last = now();
|
|
325
|
+
return async () => {
|
|
326
|
+
for (; ; ) {
|
|
327
|
+
const t = now();
|
|
328
|
+
tokens = Math.min(capacity, tokens + (t - last) * ratePerMs);
|
|
329
|
+
last = t;
|
|
330
|
+
if (tokens >= 1) {
|
|
331
|
+
tokens -= 1;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await sleep(Math.ceil((1 - tokens) / ratePerMs));
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
var createThrottle = (opts = {}) => {
|
|
339
|
+
const safety = opts.safety ?? 0.9;
|
|
340
|
+
const now = opts.now ?? (() => Date.now());
|
|
341
|
+
const read = makeBucket(Math.floor((opts.readPerMin ?? 2e3) * safety), now);
|
|
342
|
+
const write = makeBucket(Math.floor((opts.writePerMin ?? 500) * safety), now);
|
|
343
|
+
return { take: (isWrite) => isWrite ? write() : read() };
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/http/retry.ts
|
|
347
|
+
var expoBackoff = (baseMs = 200, maxMs = 8e3) => (attempt) => Math.floor(Math.random() * Math.min(maxMs, baseMs * 2 ** attempt));
|
|
348
|
+
|
|
349
|
+
// src/http/requester.ts
|
|
350
|
+
var API_VERSION = "2";
|
|
351
|
+
var MAX_REQUEST_LENGTH = 15e3;
|
|
352
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
353
|
+
var withAuth = (req, token, write) => ({
|
|
354
|
+
...req,
|
|
355
|
+
headers: {
|
|
356
|
+
...req.headers,
|
|
357
|
+
"X-porters-hrbc-oauth-token": token,
|
|
358
|
+
"X-P-ConnectAPI-Version": API_VERSION,
|
|
359
|
+
...write ? { "Content-Type": "application/xml; charset=UTF-8" } : {}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
var createRequester = (o) => {
|
|
363
|
+
const maxRetries = o.maxRetries ?? 3;
|
|
364
|
+
const request = async (req, parse, spec = {}) => {
|
|
365
|
+
const requestLength = req.url.length + (req.body?.length ?? 0);
|
|
366
|
+
if (!spec.unboundedBody && requestLength > MAX_REQUEST_LENGTH) {
|
|
367
|
+
throw new PortersConfigError(
|
|
368
|
+
`request is ${requestLength} characters, over the ~${MAX_REQUEST_LENGTH}-character limit`,
|
|
369
|
+
{
|
|
370
|
+
category: "config",
|
|
371
|
+
hint: "Shorten the request: narrow a read's field/condition, or split a write into batches of 200 or fewer records (PORTERS caps a request at ~15000 characters)."
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const write = spec.write ?? false;
|
|
376
|
+
const idempotent = spec.idempotent ?? !write;
|
|
377
|
+
let authRetried = false;
|
|
378
|
+
let forceRefresh = false;
|
|
379
|
+
let attempt = 0;
|
|
380
|
+
for (; ; ) {
|
|
381
|
+
await o.throttle.take(write);
|
|
382
|
+
try {
|
|
383
|
+
const token = await o.auth.getAccessToken(
|
|
384
|
+
forceRefresh ? { forceRefresh: true } : void 0
|
|
385
|
+
);
|
|
386
|
+
forceRefresh = false;
|
|
387
|
+
const res = await o.transport.send(withAuth(req, token, write));
|
|
388
|
+
return parse(res.body);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
if (!(e instanceof PortersError)) throw e;
|
|
391
|
+
if ((e.code === 401 || e.code === 402) && !authRetried) {
|
|
392
|
+
authRetried = true;
|
|
393
|
+
forceRefresh = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (e instanceof PortersNetworkError && write && !idempotent) throw e;
|
|
397
|
+
if (e.retryable && attempt < maxRetries) {
|
|
398
|
+
attempt += 1;
|
|
399
|
+
await sleep2(o.backoff(attempt - 1));
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
throw e;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
return { request };
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/util/datetime.ts
|
|
410
|
+
var DATETIME_RE = /^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
|
|
411
|
+
var DATE_RE = /^(\d{4})\/(\d{2})\/(\d{2})$/;
|
|
412
|
+
var ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
413
|
+
var portersDateTimeToIso = (value) => {
|
|
414
|
+
const m = DATETIME_RE.exec(value);
|
|
415
|
+
if (!m) throw new RangeError(`invalid PORTERS DateTime: "${value}"`);
|
|
416
|
+
const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
417
|
+
if (Number.isNaN(Date.parse(iso))) {
|
|
418
|
+
throw new RangeError(`invalid PORTERS DateTime: "${value}"`);
|
|
419
|
+
}
|
|
420
|
+
return iso;
|
|
421
|
+
};
|
|
422
|
+
var isoToPortersDateTime = (value) => {
|
|
423
|
+
const t = Date.parse(value);
|
|
424
|
+
if (Number.isNaN(t)) throw new RangeError(`invalid ISO datetime: "${value}"`);
|
|
425
|
+
const d = new Date(t);
|
|
426
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
427
|
+
return `${d.getUTCFullYear()}/${pad(d.getUTCMonth() + 1)}/${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
428
|
+
};
|
|
429
|
+
var portersDateToIso = (value) => {
|
|
430
|
+
const m = DATE_RE.exec(value);
|
|
431
|
+
if (!m) throw new RangeError(`invalid PORTERS Date: "${value}"`);
|
|
432
|
+
return `${m[1]}-${m[2]}-${m[3]}`;
|
|
433
|
+
};
|
|
434
|
+
var isoToPortersDate = (value) => {
|
|
435
|
+
const m = ISO_DATE_RE.exec(value);
|
|
436
|
+
if (!m) throw new RangeError(`invalid ISO date: "${value}"`);
|
|
437
|
+
return `${m[1]}/${m[2]}/${m[3]}`;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/xml/encode.ts
|
|
441
|
+
var escapeXml = (s) => s.replace(
|
|
442
|
+
/[&<>]/g,
|
|
443
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : ">"
|
|
444
|
+
);
|
|
445
|
+
var scalar = (v) => escapeXml(String(v));
|
|
446
|
+
var encodeField = (type, value) => {
|
|
447
|
+
switch (type) {
|
|
448
|
+
// Option: the selected aliases as empty child elements. Canonical input is an
|
|
449
|
+
// array (ADR-0017, symmetric with read); a lone string is wrapped as a 1-element
|
|
450
|
+
// selection (fail-safe).
|
|
451
|
+
case "Option":
|
|
452
|
+
return (Array.isArray(value) ? value : [value]).map((alias) => `<${alias}/>`).join("");
|
|
453
|
+
// System[DateTime] (registration/update) is Write-restricted by PORTERS; we still
|
|
454
|
+
// serialize it identically — rejecting the write is the input type's job (SD-3).
|
|
455
|
+
case "DateTime":
|
|
456
|
+
case "System[DateTime]":
|
|
457
|
+
return scalar(isoToPortersDateTime(String(value)));
|
|
458
|
+
// Age shares Date's wire format (`yyyy/mm/dd`): we write the birthdate.
|
|
459
|
+
case "Date":
|
|
460
|
+
case "Age":
|
|
461
|
+
return scalar(isoToPortersDate(String(value)));
|
|
462
|
+
// System[Id] / Number / User & System[Reference] (ID-only) / string Data Types all
|
|
463
|
+
// serialize as a scalar (the string types stay distinct labels per ADR-0016).
|
|
464
|
+
case "System[Id]":
|
|
465
|
+
case "Number":
|
|
466
|
+
case "User":
|
|
467
|
+
case "System[Reference]":
|
|
468
|
+
case "SinglelineText":
|
|
469
|
+
case "MultilineText":
|
|
470
|
+
case "Mail":
|
|
471
|
+
case "Telephone":
|
|
472
|
+
case "URL":
|
|
473
|
+
return scalar(value);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
var encodeItem = (prefix, fields, item) => {
|
|
477
|
+
const parts = [];
|
|
478
|
+
for (const [alias, value] of Object.entries(item)) {
|
|
479
|
+
if (value === null || value === void 0) continue;
|
|
480
|
+
const type = fields.get(alias);
|
|
481
|
+
const inner = type === void 0 ? scalar(value) : encodeField(type, value);
|
|
482
|
+
parts.push(`<${prefix}.${alias}>${inner}</${prefix}.${alias}>`);
|
|
483
|
+
}
|
|
484
|
+
return parts.join("");
|
|
485
|
+
};
|
|
486
|
+
var buildWriteXml = (config) => {
|
|
487
|
+
const items = config.items.map(
|
|
488
|
+
(item) => `<Item>${encodeItem(config.prefix, config.fields, item)}</Item>`
|
|
489
|
+
).join("");
|
|
490
|
+
return `<${config.resource}>${items}</${config.resource}>`;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/xml/decode.ts
|
|
494
|
+
var pickPrefixed = (obj, prefix, key) => asString(obj[`${prefix}.${key}`]) ?? asString(obj[key]);
|
|
495
|
+
var decodeUser = (raw) => {
|
|
496
|
+
const outer = asRecord(raw);
|
|
497
|
+
const user = outer ? asRecord(outer.User) : void 0;
|
|
498
|
+
if (!user) return null;
|
|
499
|
+
const id = pickPrefixed(user, "User", "P_Id");
|
|
500
|
+
return {
|
|
501
|
+
P_Id: id === void 0 ? null : Number(id),
|
|
502
|
+
P_Type: pickPrefixed(user, "User", "P_Type") ?? null,
|
|
503
|
+
P_Name: pickPrefixed(user, "User", "P_Name") ?? null,
|
|
504
|
+
P_Mail: pickPrefixed(user, "User", "P_Mail") ?? null
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
var decodeOption = (raw) => {
|
|
508
|
+
const outer = asRecord(raw);
|
|
509
|
+
if (!outer) return null;
|
|
510
|
+
const root = "OptionRoot" in outer ? asRecord(outer.OptionRoot) : outer;
|
|
511
|
+
if (!root) return null;
|
|
512
|
+
const keys = Object.keys(root);
|
|
513
|
+
return keys.length > 0 ? keys : null;
|
|
514
|
+
};
|
|
515
|
+
var decodeReference = (raw) => {
|
|
516
|
+
const outer = asRecord(raw);
|
|
517
|
+
if (!outer) return null;
|
|
518
|
+
for (const [tag2, value] of Object.entries(outer)) {
|
|
519
|
+
const inner = asRecord(value);
|
|
520
|
+
if (!inner) continue;
|
|
521
|
+
const id = asString(inner[`${tag2}.P_Id`]) ?? asString(inner.P_Id);
|
|
522
|
+
return id === void 0 ? null : Number(id);
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
};
|
|
526
|
+
var decodeField = (type, raw) => {
|
|
527
|
+
if (raw === "" || raw === void 0 || raw === null) return null;
|
|
528
|
+
switch (type) {
|
|
529
|
+
case "System[Id]":
|
|
530
|
+
case "Number": {
|
|
531
|
+
const s = asString(raw);
|
|
532
|
+
return s === void 0 ? null : Number(s);
|
|
533
|
+
}
|
|
534
|
+
// String Data Types share one decode (a plain string); they stay distinct
|
|
535
|
+
// labels for fidelity / future per-type validation (ADR-0016).
|
|
536
|
+
case "SinglelineText":
|
|
537
|
+
case "MultilineText":
|
|
538
|
+
case "Mail":
|
|
539
|
+
case "Telephone":
|
|
540
|
+
case "URL":
|
|
541
|
+
return asString(raw) ?? null;
|
|
542
|
+
// FT-12 DateTime and the system timestamps (registration/update) share the wire
|
|
543
|
+
// format; System[DateTime] is Write-restricted, but that is a write-time concern.
|
|
544
|
+
case "DateTime":
|
|
545
|
+
case "System[DateTime]": {
|
|
546
|
+
const s = asString(raw);
|
|
547
|
+
return s === void 0 ? null : portersDateTimeToIso(s);
|
|
548
|
+
}
|
|
549
|
+
// Age shares Date's wire format (`yyyy/mm/dd`); PORTERS transmits the birthdate
|
|
550
|
+
// and derives the age in its UI, so the faithful value is the date itself.
|
|
551
|
+
case "Date":
|
|
552
|
+
case "Age": {
|
|
553
|
+
const s = asString(raw);
|
|
554
|
+
return s === void 0 ? null : portersDateToIso(s);
|
|
555
|
+
}
|
|
556
|
+
case "User":
|
|
557
|
+
return decodeUser(raw);
|
|
558
|
+
case "Option":
|
|
559
|
+
return decodeOption(raw);
|
|
560
|
+
case "System[Reference]":
|
|
561
|
+
return decodeReference(raw);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/resources/read-core.ts
|
|
566
|
+
var bareAlias = (key) => key.includes(".") ? key.slice(key.indexOf(".") + 1) : key;
|
|
567
|
+
var decoderFor = (fields) => {
|
|
568
|
+
const fieldMap = new Map(Object.entries(fields));
|
|
569
|
+
return (item) => {
|
|
570
|
+
const out = {};
|
|
571
|
+
for (const [key, raw] of Object.entries(item)) {
|
|
572
|
+
const alias = bareAlias(key);
|
|
573
|
+
const type = fieldMap.get(alias);
|
|
574
|
+
out[alias] = type ? decodeField(type, raw) : typeof raw === "string" ? raw : null;
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
var runRead = (requester, url, decode) => requester.request({ method: "GET", url, headers: {} }, (body) => {
|
|
580
|
+
const page = parseResourcePage(body);
|
|
581
|
+
return {
|
|
582
|
+
items: page.items.map(decode),
|
|
583
|
+
total: page.total,
|
|
584
|
+
count: page.count,
|
|
585
|
+
start: page.start
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
var PAGE_SIZE = 200;
|
|
589
|
+
var paginate = async function* (fetchPage) {
|
|
590
|
+
let start = 0;
|
|
591
|
+
for (; ; ) {
|
|
592
|
+
const page = await fetchPage(PAGE_SIZE, start);
|
|
593
|
+
for (const item of page.items) yield item;
|
|
594
|
+
start += page.items.length;
|
|
595
|
+
if (page.items.length === 0 || start >= page.total) return;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/resources/resource.ts
|
|
600
|
+
var USER_SUBFIELDS = ["P_Id", "P_Type", "P_Name", "P_Mail"];
|
|
601
|
+
var defaultFieldList = (prefix, fields) => Object.entries(fields).map(
|
|
602
|
+
([alias, type]) => type === "User" ? `${prefix}.${alias}(${USER_SUBFIELDS.map((s) => `User.${s}`).join(",")})` : `${prefix}.${alias}`
|
|
603
|
+
);
|
|
604
|
+
var firstWriteResultId = (body, path, name) => {
|
|
605
|
+
const first = parseWriteResult(body)[0];
|
|
606
|
+
if (first === void 0) {
|
|
607
|
+
throw new PortersResourceError("write returned no result item", {
|
|
608
|
+
category: "unknown"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (first.code !== 0) {
|
|
612
|
+
throw resourceError(
|
|
613
|
+
first.code,
|
|
614
|
+
`${path} write returned code ${first.code}`,
|
|
615
|
+
{
|
|
616
|
+
resource: name
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
return first.id;
|
|
621
|
+
};
|
|
622
|
+
var buildReadUrl = (host, partition, path, q) => {
|
|
623
|
+
const p = new URLSearchParams();
|
|
624
|
+
p.set("partition", String(partition));
|
|
625
|
+
if (q.field && q.field.length > 0) p.set("field", q.field.join(","));
|
|
626
|
+
if (q.condition) {
|
|
627
|
+
const conds = Object.entries(q.condition).map(([k, v]) => `${k}=${v}`);
|
|
628
|
+
if (conds.length > 0) p.set("condition", conds.join(","));
|
|
629
|
+
}
|
|
630
|
+
if (q.count !== void 0) p.set("count", String(q.count));
|
|
631
|
+
if (q.start !== void 0) p.set("start", String(q.start));
|
|
632
|
+
return `https://${host}/v1/${path}?${p.toString()}`;
|
|
633
|
+
};
|
|
634
|
+
var buildWriteUrl = (host, partition, path) => `https://${host}/v1/${path}?partition=${partition}`;
|
|
635
|
+
var createResource = (config, deps) => {
|
|
636
|
+
const fieldMap = new Map(Object.entries(config.fields));
|
|
637
|
+
const decode = decoderFor(config.fields);
|
|
638
|
+
const defaultFields = defaultFieldList(config.prefix, config.fields);
|
|
639
|
+
const readUrl = (q) => buildReadUrl(deps.host, deps.partition, config.path, q);
|
|
640
|
+
const writeUrl = () => buildWriteUrl(deps.host, deps.partition, config.path);
|
|
641
|
+
const firstWriteId = (body) => firstWriteResultId(body, config.path, config.name);
|
|
642
|
+
const search = (query = {}) => runRead(
|
|
643
|
+
deps.requester,
|
|
644
|
+
readUrl({ ...query, field: query.field ?? defaultFields }),
|
|
645
|
+
decode
|
|
646
|
+
);
|
|
647
|
+
const searchAll = (query = {}) => paginate((count, start) => search({ ...query, count, start }));
|
|
648
|
+
const get = async (id) => {
|
|
649
|
+
const page = await search({
|
|
650
|
+
condition: { [`${config.prefix}.P_Id:eq`]: String(id) },
|
|
651
|
+
count: 1
|
|
652
|
+
});
|
|
653
|
+
return page.items[0];
|
|
654
|
+
};
|
|
655
|
+
const write = (item, idempotent) => deps.requester.request(
|
|
656
|
+
{
|
|
657
|
+
method: "POST",
|
|
658
|
+
url: writeUrl(),
|
|
659
|
+
headers: {},
|
|
660
|
+
body: buildWriteXml({
|
|
661
|
+
resource: config.name,
|
|
662
|
+
prefix: config.prefix,
|
|
663
|
+
fields: fieldMap,
|
|
664
|
+
items: [item]
|
|
665
|
+
})
|
|
666
|
+
},
|
|
667
|
+
firstWriteId,
|
|
668
|
+
{ write: true, idempotent }
|
|
669
|
+
);
|
|
670
|
+
const create = (input) => write({ ...input, P_Id: -1 }, false);
|
|
671
|
+
const update = (id, input) => write({ ...input, P_Id: id }, true);
|
|
672
|
+
return { search, searchAll, get, create, update };
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/resources/candidate.ts
|
|
676
|
+
var FIELDS = {
|
|
677
|
+
P_Id: "System[Id]",
|
|
678
|
+
P_Owner: "User",
|
|
679
|
+
P_RegistrationDate: "System[DateTime]",
|
|
680
|
+
P_RegisteredBy: "User",
|
|
681
|
+
P_UpdateDate: "System[DateTime]",
|
|
682
|
+
P_UpdatedBy: "User",
|
|
683
|
+
P_Phase: "Option",
|
|
684
|
+
P_PhaseDate: "DateTime",
|
|
685
|
+
P_Name: "SinglelineText",
|
|
686
|
+
P_Reading: "SinglelineText",
|
|
687
|
+
P_Mail: "Mail",
|
|
688
|
+
P_MobileMail: "Mail",
|
|
689
|
+
P_Telephone: "Telephone",
|
|
690
|
+
P_Mobile: "Telephone",
|
|
691
|
+
P_Country: "SinglelineText",
|
|
692
|
+
P_Prefecture: "SinglelineText",
|
|
693
|
+
P_City: "SinglelineText",
|
|
694
|
+
P_Zipcode: "SinglelineText"
|
|
695
|
+
};
|
|
696
|
+
var REQUIRED_ON_CREATE = [
|
|
697
|
+
"P_Owner"
|
|
698
|
+
];
|
|
699
|
+
var createCandidateResource = (deps, custom) => {
|
|
700
|
+
const fields = { ...FIELDS, ...custom };
|
|
701
|
+
return createResource(
|
|
702
|
+
{
|
|
703
|
+
name: "Candidate",
|
|
704
|
+
path: "candidate",
|
|
705
|
+
prefix: "Person",
|
|
706
|
+
fields,
|
|
707
|
+
requiredOnCreate: REQUIRED_ON_CREATE
|
|
708
|
+
},
|
|
709
|
+
deps
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// src/resources/job.ts
|
|
714
|
+
var FIELDS2 = {
|
|
715
|
+
P_Id: "System[Id]",
|
|
716
|
+
P_Owner: "User",
|
|
717
|
+
P_Client: "System[Reference]",
|
|
718
|
+
P_Recruiter: "System[Reference]",
|
|
719
|
+
P_RegistrationDate: "System[DateTime]",
|
|
720
|
+
P_RegisteredBy: "User",
|
|
721
|
+
P_UpdateDate: "System[DateTime]",
|
|
722
|
+
P_UpdatedBy: "User",
|
|
723
|
+
P_Phase: "Option",
|
|
724
|
+
P_PhaseDate: "DateTime",
|
|
725
|
+
P_PhaseMemo: "MultilineText",
|
|
726
|
+
P_Position: "SinglelineText",
|
|
727
|
+
P_Publish: "Option",
|
|
728
|
+
P_JobCategorySummary: "MultilineText",
|
|
729
|
+
P_JobCategory: "Option",
|
|
730
|
+
P_IndustrySummary: "MultilineText",
|
|
731
|
+
P_Industry: "Option",
|
|
732
|
+
P_SalarySummary: "MultilineText",
|
|
733
|
+
P_MinSalary: "Number",
|
|
734
|
+
P_MaxSalary: "Number",
|
|
735
|
+
P_AreaSummary: "MultilineText",
|
|
736
|
+
P_Area: "Option",
|
|
737
|
+
P_PayrollsText: "SinglelineText",
|
|
738
|
+
P_Memo: "MultilineText",
|
|
739
|
+
P_EmploymentPeriod: "MultilineText",
|
|
740
|
+
// "P_WokingHours" is the alias as published by PORTERS (source typo); keep verbatim.
|
|
741
|
+
P_WokingHours: "MultilineText",
|
|
742
|
+
P_Holidays: "MultilineText",
|
|
743
|
+
P_Benefits: "MultilineText",
|
|
744
|
+
P_PubliclyTraded: "Option",
|
|
745
|
+
P_SalesAmountText: "SinglelineText",
|
|
746
|
+
P_EstablishmentDateText: "SinglelineText",
|
|
747
|
+
P_CapitalText: "SinglelineText",
|
|
748
|
+
P_EmploymentType: "Option",
|
|
749
|
+
P_ExpectedAgeReason: "Option"
|
|
750
|
+
};
|
|
751
|
+
var REQUIRED_ON_CREATE2 = [
|
|
752
|
+
"P_Owner",
|
|
753
|
+
"P_Client",
|
|
754
|
+
"P_Recruiter"
|
|
755
|
+
];
|
|
756
|
+
var createJobResource = (deps, custom) => {
|
|
757
|
+
const fields = { ...FIELDS2, ...custom };
|
|
758
|
+
return createResource(
|
|
759
|
+
{
|
|
760
|
+
name: "Job",
|
|
761
|
+
path: "job",
|
|
762
|
+
prefix: "Job",
|
|
763
|
+
fields,
|
|
764
|
+
requiredOnCreate: REQUIRED_ON_CREATE2
|
|
765
|
+
},
|
|
766
|
+
deps
|
|
767
|
+
);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// src/resources/client.ts
|
|
771
|
+
var FIELDS3 = {
|
|
772
|
+
P_Id: "System[Id]",
|
|
773
|
+
P_Owner: "User",
|
|
774
|
+
P_RegistrationDate: "System[DateTime]",
|
|
775
|
+
P_RegisteredBy: "User",
|
|
776
|
+
P_UpdateDate: "System[DateTime]",
|
|
777
|
+
P_UpdatedBy: "User",
|
|
778
|
+
P_Phase: "Option",
|
|
779
|
+
P_PhaseDate: "DateTime",
|
|
780
|
+
P_PhaseMemo: "MultilineText",
|
|
781
|
+
P_Name: "SinglelineText",
|
|
782
|
+
P_Memo: "MultilineText",
|
|
783
|
+
P_Country: "SinglelineText",
|
|
784
|
+
P_Prefecture: "SinglelineText",
|
|
785
|
+
P_City: "SinglelineText",
|
|
786
|
+
P_Street: "MultilineText",
|
|
787
|
+
P_Zipcode: "SinglelineText",
|
|
788
|
+
P_Telephone: "Telephone",
|
|
789
|
+
P_Fax: "Telephone"
|
|
790
|
+
};
|
|
791
|
+
var REQUIRED_ON_CREATE3 = [
|
|
792
|
+
"P_Owner"
|
|
793
|
+
];
|
|
794
|
+
var createClientResource = (deps, custom) => {
|
|
795
|
+
const fields = { ...FIELDS3, ...custom };
|
|
796
|
+
return createResource(
|
|
797
|
+
{
|
|
798
|
+
name: "Client",
|
|
799
|
+
path: "client",
|
|
800
|
+
prefix: "Client",
|
|
801
|
+
fields,
|
|
802
|
+
requiredOnCreate: REQUIRED_ON_CREATE3
|
|
803
|
+
},
|
|
804
|
+
deps
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/resources/process.ts
|
|
809
|
+
var FIELDS4 = {
|
|
810
|
+
P_Id: "System[Id]",
|
|
811
|
+
P_Owner: "User",
|
|
812
|
+
P_Client: "System[Reference]",
|
|
813
|
+
P_Recruiter: "System[Reference]",
|
|
814
|
+
P_Job: "System[Reference]",
|
|
815
|
+
P_Candidate: "System[Reference]",
|
|
816
|
+
P_Resume: "System[Reference]",
|
|
817
|
+
P_RegistrationDate: "System[DateTime]",
|
|
818
|
+
P_RegisteredBy: "User",
|
|
819
|
+
P_UpdateDate: "System[DateTime]",
|
|
820
|
+
P_UpdatedBy: "User",
|
|
821
|
+
P_Phase: "Option",
|
|
822
|
+
P_PhaseDate: "DateTime",
|
|
823
|
+
P_PhaseMemo: "MultilineText",
|
|
824
|
+
P_Close: "Option",
|
|
825
|
+
P_CloseReason: "Option",
|
|
826
|
+
P_ExpectedSalesAmount: "Number",
|
|
827
|
+
P_ExpectedClosingDate: "Date"
|
|
828
|
+
};
|
|
829
|
+
var REQUIRED_ON_CREATE4 = [
|
|
830
|
+
"P_Owner",
|
|
831
|
+
"P_Client",
|
|
832
|
+
"P_Recruiter",
|
|
833
|
+
"P_Job",
|
|
834
|
+
"P_Candidate",
|
|
835
|
+
"P_Resume"
|
|
836
|
+
];
|
|
837
|
+
var createProcessResource = (deps, custom) => {
|
|
838
|
+
const fields = { ...FIELDS4, ...custom };
|
|
839
|
+
return createResource(
|
|
840
|
+
{
|
|
841
|
+
name: "Process",
|
|
842
|
+
path: "process",
|
|
843
|
+
prefix: "Process",
|
|
844
|
+
fields,
|
|
845
|
+
requiredOnCreate: REQUIRED_ON_CREATE4
|
|
846
|
+
},
|
|
847
|
+
deps
|
|
848
|
+
);
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/resources/resume.ts
|
|
852
|
+
var FIELDS5 = {
|
|
853
|
+
P_Id: "System[Id]",
|
|
854
|
+
P_Owner: "User",
|
|
855
|
+
P_Candidate: "System[Reference]",
|
|
856
|
+
P_RegistrationDate: "System[DateTime]",
|
|
857
|
+
P_RegisteredBy: "User",
|
|
858
|
+
P_UpdateDate: "System[DateTime]",
|
|
859
|
+
P_UpdatedBy: "User",
|
|
860
|
+
P_Phase: "Option",
|
|
861
|
+
P_PhaseDate: "DateTime",
|
|
862
|
+
P_PhaseMemo: "MultilineText",
|
|
863
|
+
P_Name: "SinglelineText",
|
|
864
|
+
P_RegisterChannel: "Option",
|
|
865
|
+
P_Memo: "MultilineText",
|
|
866
|
+
P_CurrentStatus: "Option",
|
|
867
|
+
P_Education: "MultilineText",
|
|
868
|
+
P_CarrierSummary: "MultilineText",
|
|
869
|
+
P_CurrentSalary: "Number",
|
|
870
|
+
P_ExperiencedJobCategory: "Option",
|
|
871
|
+
P_ExperiencedIndustry: "Option",
|
|
872
|
+
P_ChangeJobsCount: "Number",
|
|
873
|
+
P_Gender: "Option",
|
|
874
|
+
P_DateOfBirth: "Age",
|
|
875
|
+
P_ExpectEmploymentType: "Option",
|
|
876
|
+
P_ExpectArea: "Option",
|
|
877
|
+
P_ExpectJobCategory: "Option",
|
|
878
|
+
P_ExpectIndustry: "Option",
|
|
879
|
+
P_ExpectCondition: "MultilineText",
|
|
880
|
+
P_ExpectSalary: "Number",
|
|
881
|
+
P_DesiredHourlyRate: "Number",
|
|
882
|
+
P_HourlyRate: "Number"
|
|
883
|
+
};
|
|
884
|
+
var REQUIRED_ON_CREATE5 = [
|
|
885
|
+
"P_Owner",
|
|
886
|
+
"P_Candidate"
|
|
887
|
+
];
|
|
888
|
+
var createResumeResource = (deps, custom) => {
|
|
889
|
+
const fields = { ...FIELDS5, ...custom };
|
|
890
|
+
return createResource(
|
|
891
|
+
{
|
|
892
|
+
name: "Resume",
|
|
893
|
+
path: "resume",
|
|
894
|
+
prefix: "Resume",
|
|
895
|
+
fields,
|
|
896
|
+
requiredOnCreate: REQUIRED_ON_CREATE5
|
|
897
|
+
},
|
|
898
|
+
deps
|
|
899
|
+
);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/resources/attachment.ts
|
|
903
|
+
var MAX_CONTENT_CHARS = 14e6;
|
|
904
|
+
var ALL_FIELDS = [
|
|
905
|
+
"Id",
|
|
906
|
+
"Resource",
|
|
907
|
+
"ResourceId",
|
|
908
|
+
"ContentType",
|
|
909
|
+
"FileName",
|
|
910
|
+
"Content"
|
|
911
|
+
];
|
|
912
|
+
var DEFAULT_FIELDS = [
|
|
913
|
+
"Id",
|
|
914
|
+
"Resource",
|
|
915
|
+
"ResourceId",
|
|
916
|
+
"ContentType",
|
|
917
|
+
"FileName"
|
|
918
|
+
];
|
|
919
|
+
var numOrNull = (v) => {
|
|
920
|
+
const s = asString(v);
|
|
921
|
+
return s === void 0 ? null : Number(s);
|
|
922
|
+
};
|
|
923
|
+
var decodeAttachment = (item) => ({
|
|
924
|
+
id: numOrNull(item.Id),
|
|
925
|
+
resource: numOrNull(item.Resource),
|
|
926
|
+
resourceId: numOrNull(item.ResourceId),
|
|
927
|
+
contentType: asString(item.ContentType) ?? null,
|
|
928
|
+
fileName: asString(item.FileName) ?? null,
|
|
929
|
+
content: asString(item.Content) ?? null
|
|
930
|
+
});
|
|
931
|
+
var tag = (name, value) => `<${name}>${encodeField("SinglelineText", String(value))}</${name}>`;
|
|
932
|
+
var guardContent = (content) => {
|
|
933
|
+
if (content !== void 0 && content.length > MAX_CONTENT_CHARS) {
|
|
934
|
+
throw new PortersConfigError(
|
|
935
|
+
`attachment content is ${content.length} characters, over the ~10MB file limit`,
|
|
936
|
+
{ category: "config", hint: "Attachment files must be 10MB or less." }
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
var createAttachmentResource = (deps) => {
|
|
941
|
+
const search = (query = {}) => deps.requester.request(
|
|
942
|
+
{
|
|
943
|
+
method: "GET",
|
|
944
|
+
url: buildReadUrl(deps.host, deps.partition, "attachment", {
|
|
945
|
+
...query,
|
|
946
|
+
field: query.field ?? DEFAULT_FIELDS
|
|
947
|
+
}),
|
|
948
|
+
headers: {}
|
|
949
|
+
},
|
|
950
|
+
(body) => {
|
|
951
|
+
const page = parseResourcePage(body);
|
|
952
|
+
return {
|
|
953
|
+
items: page.items.map(decodeAttachment),
|
|
954
|
+
total: page.total,
|
|
955
|
+
count: page.count,
|
|
956
|
+
start: page.start
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
const get = async (id) => {
|
|
961
|
+
const page = await search({
|
|
962
|
+
field: ALL_FIELDS,
|
|
963
|
+
condition: { "Id:eq": String(id) },
|
|
964
|
+
count: 1
|
|
965
|
+
});
|
|
966
|
+
return page.items[0];
|
|
967
|
+
};
|
|
968
|
+
const write = (inner, idempotent) => deps.requester.request(
|
|
969
|
+
{
|
|
970
|
+
method: "POST",
|
|
971
|
+
url: buildWriteUrl(deps.host, deps.partition, "attachment"),
|
|
972
|
+
headers: {},
|
|
973
|
+
body: `<Attachment><Item>${inner}</Item></Attachment>`
|
|
974
|
+
},
|
|
975
|
+
(body) => firstWriteResultId(body, "attachment", "Attachment"),
|
|
976
|
+
{ write: true, idempotent, unboundedBody: true }
|
|
977
|
+
);
|
|
978
|
+
const create = (input) => {
|
|
979
|
+
guardContent(input.content);
|
|
980
|
+
const inner = tag("Id", -1) + tag("Resource", input.resource) + tag("ResourceId", input.resourceId) + tag("ContentType", input.contentType) + tag("FileName", input.fileName) + tag("Content", input.content);
|
|
981
|
+
return write(inner, false);
|
|
982
|
+
};
|
|
983
|
+
const update = (id, input) => {
|
|
984
|
+
guardContent(input.content);
|
|
985
|
+
let inner = tag("Id", id);
|
|
986
|
+
if (input.contentType !== void 0) {
|
|
987
|
+
inner += tag("ContentType", input.contentType);
|
|
988
|
+
}
|
|
989
|
+
if (input.fileName !== void 0) inner += tag("FileName", input.fileName);
|
|
990
|
+
if (input.content !== void 0) inner += tag("Content", input.content);
|
|
991
|
+
return write(inner, true);
|
|
992
|
+
};
|
|
993
|
+
return { search, get, create, update };
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// src/resources/partition.ts
|
|
997
|
+
var FIELDS6 = {
|
|
998
|
+
P_Id: "System[Id]",
|
|
999
|
+
P_Name: "SinglelineText",
|
|
1000
|
+
P_CompanyId: "SinglelineText"
|
|
1001
|
+
};
|
|
1002
|
+
var buildUrl = (host, q) => {
|
|
1003
|
+
const p = new URLSearchParams();
|
|
1004
|
+
p.set("request_type", String(q.requestType ?? 1));
|
|
1005
|
+
if (q.count !== void 0) p.set("count", String(q.count));
|
|
1006
|
+
if (q.start !== void 0) p.set("start", String(q.start));
|
|
1007
|
+
return `https://${host}/v1/partition?${p.toString()}`;
|
|
1008
|
+
};
|
|
1009
|
+
var createPartitionResource = (deps) => {
|
|
1010
|
+
const decode = decoderFor(FIELDS6);
|
|
1011
|
+
const search = (query = {}) => runRead(deps.requester, buildUrl(deps.host, query), decode);
|
|
1012
|
+
const searchAll = (query = {}) => paginate((count, start) => search({ ...query, count, start }));
|
|
1013
|
+
return { search, searchAll };
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// src/resources/user.ts
|
|
1017
|
+
var FIELDS7 = {
|
|
1018
|
+
P_Id: "System[Id]",
|
|
1019
|
+
P_Type: "Number",
|
|
1020
|
+
P_Name: "SinglelineText",
|
|
1021
|
+
P_Mail: "Mail"
|
|
1022
|
+
};
|
|
1023
|
+
var buildUrl2 = (host, partition, q) => {
|
|
1024
|
+
const p = new URLSearchParams();
|
|
1025
|
+
p.set("partition", String(partition));
|
|
1026
|
+
p.set("request_type", String(q.requestType ?? 1));
|
|
1027
|
+
p.set("user_type", String(q.userType ?? -1));
|
|
1028
|
+
if (q.field && q.field.length > 0) p.set("field", q.field.join(","));
|
|
1029
|
+
if (q.count !== void 0) p.set("count", String(q.count));
|
|
1030
|
+
if (q.start !== void 0) p.set("start", String(q.start));
|
|
1031
|
+
return `https://${host}/v1/user?${p.toString()}`;
|
|
1032
|
+
};
|
|
1033
|
+
var createUserResource = (deps) => {
|
|
1034
|
+
const decode = decoderFor(FIELDS7);
|
|
1035
|
+
const search = (query = {}) => runRead(deps.requester, buildUrl2(deps.host, deps.partition, query), decode);
|
|
1036
|
+
const searchAll = (query = {}) => paginate((count, start) => search({ ...query, count, start }));
|
|
1037
|
+
const current = async () => (await search({ requestType: 0, count: 1 })).items[0];
|
|
1038
|
+
return { search, searchAll, current };
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/resources/field.ts
|
|
1042
|
+
var RESOURCE_VALUE = {
|
|
1043
|
+
candidate: 1,
|
|
1044
|
+
job: 3,
|
|
1045
|
+
client: 5,
|
|
1046
|
+
recruiter: 9,
|
|
1047
|
+
sales: 11,
|
|
1048
|
+
contract: 13,
|
|
1049
|
+
resume: 17,
|
|
1050
|
+
activity: 19,
|
|
1051
|
+
opportunity: 25,
|
|
1052
|
+
contact: 27
|
|
1053
|
+
};
|
|
1054
|
+
var FIELDS8 = {
|
|
1055
|
+
P_Id: "System[Id]",
|
|
1056
|
+
P_Name: "SinglelineText",
|
|
1057
|
+
P_Alias: "SinglelineText",
|
|
1058
|
+
P_Type: "Number",
|
|
1059
|
+
P_Required: "Number",
|
|
1060
|
+
P_Max: "Number",
|
|
1061
|
+
P_Min: "Number",
|
|
1062
|
+
P_DecimalFraction: "Number",
|
|
1063
|
+
// Nested alias(es): the referenced option group / parent field. Reuse Option decode -> the
|
|
1064
|
+
// referenced alias(es) as string[] (usually one); empty -> null (ADR-0017/0022).
|
|
1065
|
+
// VERIFY(live): Reference-type P_ReferTo nesting is doc-only. See docs/live-verification.md (LV-6).
|
|
1066
|
+
P_ReferTo: "Option",
|
|
1067
|
+
P_ResourceType: "Number"
|
|
1068
|
+
};
|
|
1069
|
+
var buildUrl3 = (host, partition, q) => {
|
|
1070
|
+
const p = new URLSearchParams();
|
|
1071
|
+
p.set("partition", String(partition));
|
|
1072
|
+
p.set("resource", String(RESOURCE_VALUE[q.resource]));
|
|
1073
|
+
p.set("active", String(q.active ?? -1));
|
|
1074
|
+
if (q.count !== void 0) p.set("count", String(q.count));
|
|
1075
|
+
if (q.start !== void 0) p.set("start", String(q.start));
|
|
1076
|
+
return `https://${host}/v1/field?${p.toString()}`;
|
|
1077
|
+
};
|
|
1078
|
+
var createFieldResource = (deps) => {
|
|
1079
|
+
const decode = decoderFor(FIELDS8);
|
|
1080
|
+
const search = (query) => runRead(deps.requester, buildUrl3(deps.host, deps.partition, query), decode);
|
|
1081
|
+
const searchAll = (query) => paginate((count, start) => search({ ...query, count, start }));
|
|
1082
|
+
return { search, searchAll };
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
// src/resources/option.ts
|
|
1086
|
+
var FIELDS9 = {
|
|
1087
|
+
P_Id: "System[Id]",
|
|
1088
|
+
P_Name: "SinglelineText",
|
|
1089
|
+
P_Alias: "SinglelineText",
|
|
1090
|
+
P_ParentId: "Number",
|
|
1091
|
+
P_Type: "Number",
|
|
1092
|
+
P_Order: "Number"
|
|
1093
|
+
};
|
|
1094
|
+
var buildUrl4 = (host, partition, q) => {
|
|
1095
|
+
const p = new URLSearchParams();
|
|
1096
|
+
p.set("partition", String(partition));
|
|
1097
|
+
if (q.alias !== void 0) p.set("alias", q.alias);
|
|
1098
|
+
if (q.level !== void 0) p.set("level", String(q.level));
|
|
1099
|
+
if (q.enabled !== void 0) p.set("enabled", String(q.enabled));
|
|
1100
|
+
if (q.count !== void 0) p.set("count", String(q.count));
|
|
1101
|
+
return `https://${host}/v1/option?${p.toString()}`;
|
|
1102
|
+
};
|
|
1103
|
+
var withoutItems = (raw) => Object.fromEntries(Object.entries(raw).filter(([k]) => k !== "Items"));
|
|
1104
|
+
var createOptionResource = (deps) => {
|
|
1105
|
+
const decode = decoderFor(FIELDS9);
|
|
1106
|
+
const flatten = (items, out) => {
|
|
1107
|
+
for (const raw of items) {
|
|
1108
|
+
out.push(decode(withoutItems(raw)));
|
|
1109
|
+
const children = asArray(asRecord(raw.Items)?.Item).map(
|
|
1110
|
+
(it) => asRecord(it) ?? {}
|
|
1111
|
+
);
|
|
1112
|
+
flatten(children, out);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
const search = (query = {}) => deps.requester.request(
|
|
1116
|
+
{
|
|
1117
|
+
method: "GET",
|
|
1118
|
+
url: buildUrl4(deps.host, deps.partition, query),
|
|
1119
|
+
headers: {}
|
|
1120
|
+
},
|
|
1121
|
+
(body) => {
|
|
1122
|
+
const out = [];
|
|
1123
|
+
flatten(parseResourcePage(body).items, out);
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
return { search };
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// src/client.ts
|
|
1131
|
+
var PortersClient = class {
|
|
1132
|
+
candidate;
|
|
1133
|
+
job;
|
|
1134
|
+
client;
|
|
1135
|
+
process;
|
|
1136
|
+
resume;
|
|
1137
|
+
attachment;
|
|
1138
|
+
/** Master Read: accessible partitions (ADR-0021/0022). */
|
|
1139
|
+
partition;
|
|
1140
|
+
/** Master Read: users, plus `current()` self-identification (ADR-0021/0022). */
|
|
1141
|
+
user;
|
|
1142
|
+
/** Master Read: a resource's field catalog (ADR-0021/0022). */
|
|
1143
|
+
field;
|
|
1144
|
+
/** Master Read: a tenant's choice (option) master (ADR-0021/0022). */
|
|
1145
|
+
option;
|
|
1146
|
+
#host;
|
|
1147
|
+
constructor(options) {
|
|
1148
|
+
const transport = options.transport ?? createFetchTransport();
|
|
1149
|
+
const auth = options.auth ?? createDefaultTokenProvider({
|
|
1150
|
+
host: options.host,
|
|
1151
|
+
appId: options.appId ?? "",
|
|
1152
|
+
appSecret: options.appSecret ?? "",
|
|
1153
|
+
transport,
|
|
1154
|
+
tokenStore: options.tokenStore
|
|
1155
|
+
});
|
|
1156
|
+
const requester = createRequester({
|
|
1157
|
+
transport,
|
|
1158
|
+
auth,
|
|
1159
|
+
throttle: createThrottle(),
|
|
1160
|
+
backoff: expoBackoff()
|
|
1161
|
+
});
|
|
1162
|
+
this.#host = options.host;
|
|
1163
|
+
const deps = {
|
|
1164
|
+
requester,
|
|
1165
|
+
host: options.host,
|
|
1166
|
+
partition: options.partition ?? 0
|
|
1167
|
+
};
|
|
1168
|
+
const customFor = (key) => options.fields?.[key] ?? {};
|
|
1169
|
+
this.candidate = createCandidateResource(deps, customFor("candidate"));
|
|
1170
|
+
this.job = createJobResource(deps, customFor("job"));
|
|
1171
|
+
this.client = createClientResource(deps, customFor("client"));
|
|
1172
|
+
this.process = createProcessResource(deps, customFor("process"));
|
|
1173
|
+
this.resume = createResumeResource(deps, customFor("resume"));
|
|
1174
|
+
this.attachment = createAttachmentResource(deps);
|
|
1175
|
+
this.partition = createPartitionResource({ requester, host: options.host });
|
|
1176
|
+
this.user = createUserResource(deps);
|
|
1177
|
+
this.field = createFieldResource(deps);
|
|
1178
|
+
this.option = createOptionResource(deps);
|
|
1179
|
+
}
|
|
1180
|
+
/** The configured API host. */
|
|
1181
|
+
get host() {
|
|
1182
|
+
return this.#host;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/fields/define-fields.ts
|
|
1187
|
+
var def = (dataType) => ({
|
|
1188
|
+
dataType
|
|
1189
|
+
});
|
|
1190
|
+
var builder = {
|
|
1191
|
+
number: () => def("Number"),
|
|
1192
|
+
singlelineText: () => def("SinglelineText"),
|
|
1193
|
+
multilineText: () => def("MultilineText"),
|
|
1194
|
+
mail: () => def("Mail"),
|
|
1195
|
+
telephone: () => def("Telephone"),
|
|
1196
|
+
url: () => def("URL"),
|
|
1197
|
+
date: () => def("Date"),
|
|
1198
|
+
dateTime: () => def("DateTime"),
|
|
1199
|
+
age: () => def("Age"),
|
|
1200
|
+
option: () => def("Option"),
|
|
1201
|
+
user: () => def("User")
|
|
1202
|
+
};
|
|
1203
|
+
var ALIAS_PATTERN = /^[UA]_/;
|
|
1204
|
+
var KNOWN_RESOURCES = [
|
|
1205
|
+
"candidate",
|
|
1206
|
+
"job",
|
|
1207
|
+
"client",
|
|
1208
|
+
"process",
|
|
1209
|
+
"resume"
|
|
1210
|
+
];
|
|
1211
|
+
var defineFields = (decls) => {
|
|
1212
|
+
const catalogs = {};
|
|
1213
|
+
for (const [resource, declare] of Object.entries(decls)) {
|
|
1214
|
+
if (declare === void 0) continue;
|
|
1215
|
+
if (!KNOWN_RESOURCES.includes(resource)) {
|
|
1216
|
+
throw new PortersConfigError(
|
|
1217
|
+
`defineFields: unknown resource "${resource}" (expected one of ${KNOWN_RESOURCES.join(", ")})`,
|
|
1218
|
+
{ category: "config" }
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const catalog = {};
|
|
1222
|
+
for (const [alias, fieldDef] of Object.entries(declare(builder))) {
|
|
1223
|
+
if (!ALIAS_PATTERN.test(alias)) {
|
|
1224
|
+
throw new PortersConfigError(
|
|
1225
|
+
`defineFields: custom field alias "${alias}" on "${resource}" must start with "U_" or "A_" (standard P_ fields are built in)`,
|
|
1226
|
+
{ category: "config" }
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
catalog[alias] = fieldDef.dataType;
|
|
1230
|
+
}
|
|
1231
|
+
catalogs[resource] = catalog;
|
|
1232
|
+
}
|
|
1233
|
+
return Object.freeze(catalogs);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// src/util/base64.ts
|
|
1237
|
+
var bytesToBase64 = (bytes) => {
|
|
1238
|
+
let binary = "";
|
|
1239
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
1240
|
+
return btoa(binary);
|
|
1241
|
+
};
|
|
1242
|
+
var base64ToBytes = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
1243
|
+
export {
|
|
1244
|
+
PortersAuthError,
|
|
1245
|
+
PortersClient,
|
|
1246
|
+
PortersConfigError,
|
|
1247
|
+
PortersError,
|
|
1248
|
+
PortersNetworkError,
|
|
1249
|
+
PortersResourceError,
|
|
1250
|
+
base64ToBytes,
|
|
1251
|
+
bytesToBase64,
|
|
1252
|
+
createMockTransport,
|
|
1253
|
+
defineFields
|
|
1254
|
+
};
|
|
1255
|
+
//# sourceMappingURL=index.js.map
|