@openfn/language-collections 1.0.0-next-13f27ea6

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,318 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/collections.js
8
+ var collections_exports = {};
9
+ __export(collections_exports, {
10
+ createMockServer: () => createServer,
11
+ each: () => each,
12
+ expandQuery: () => expandQuery,
13
+ get: () => get,
14
+ remove: () => remove,
15
+ request: () => request,
16
+ set: () => set,
17
+ setMockClient: () => setMockClient,
18
+ streamResponse: () => streamResponse
19
+ });
20
+ import nodepath from "path";
21
+ import undici from "undici";
22
+ import { throwError, expandReferences } from "@openfn/language-common/util";
23
+ import chain from "stream-chain";
24
+ import parser from "stream-json";
25
+ import Pick from "stream-json/filters/Pick";
26
+ import streamArray from "stream-json/streamers/StreamArray";
27
+
28
+ // src/mock.js
29
+ import { MockAgent } from "undici";
30
+ var COLLECTION_NOT_FOUND = "COLLECTION_NOT_FOUND";
31
+ var INVALID_AUTH = "INVALID_AUTH";
32
+ function API() {
33
+ let collections = {};
34
+ const createCollection = (name, values = {}) => {
35
+ collections[name] = values;
36
+ };
37
+ const reset = () => {
38
+ collections = api.collections = {};
39
+ };
40
+ const upsert = (name, key, value) => {
41
+ if (!(name in collections)) {
42
+ throw new Error(COLLECTION_NOT_FOUND);
43
+ }
44
+ collections[name][key] = value;
45
+ };
46
+ const fetch = (name, key, query) => {
47
+ if (!(name in collections)) {
48
+ throw new Error(COLLECTION_NOT_FOUND);
49
+ }
50
+ const col = collections[name];
51
+ const results = [];
52
+ const regex = new RegExp(key.replace("*", "(.*)"));
53
+ for (const key2 in col) {
54
+ if (regex.test(key2)) {
55
+ results.push({
56
+ key: key2,
57
+ value: col[key2]
58
+ });
59
+ }
60
+ }
61
+ return { results };
62
+ };
63
+ const byKey = (name, key) => {
64
+ return collections[name][key];
65
+ };
66
+ const remove2 = (name, key) => {
67
+ if (!(name in collections)) {
68
+ throw new Error(COLLECTION_NOT_FOUND);
69
+ }
70
+ const col = collections[name];
71
+ delete col[key];
72
+ };
73
+ const api = {
74
+ collections,
75
+ createCollection,
76
+ reset,
77
+ upsert,
78
+ fetch,
79
+ remove: remove2,
80
+ byKey
81
+ };
82
+ return api;
83
+ }
84
+ var parsePath = (path) => {
85
+ let [_collections, name, key] = path.split("/");
86
+ return { name, key };
87
+ };
88
+ var assertAuth = (req) => {
89
+ const auth = req.headers.Authorization;
90
+ if (!auth) {
91
+ throw new Error(INVALID_AUTH);
92
+ }
93
+ };
94
+ function createServer(url = "https://app.openfn.org") {
95
+ const agent = new MockAgent();
96
+ agent.disableNetConnect();
97
+ const mockPool = agent.get(url);
98
+ const api = new API();
99
+ const get2 = (req) => {
100
+ try {
101
+ assertAuth(req);
102
+ } catch (e) {
103
+ return { statusCode: 403 };
104
+ }
105
+ try {
106
+ let { name, key } = parsePath(req.path);
107
+ if (!key) {
108
+ key = "*";
109
+ }
110
+ const body = api.fetch(name, key);
111
+ return {
112
+ statusCode: 200,
113
+ data: JSON.stringify(body),
114
+ responseOptions: {
115
+ headers: { "Content-Type": "application/json" }
116
+ }
117
+ };
118
+ } catch (e) {
119
+ if (e.message === COLLECTION_NOT_FOUND) {
120
+ return { statusCode: 404 };
121
+ }
122
+ }
123
+ return { statusCode: 500 };
124
+ };
125
+ const post = (req) => {
126
+ try {
127
+ assertAuth(req);
128
+ } catch (e) {
129
+ return { statusCode: 403 };
130
+ }
131
+ try {
132
+ const { name, key } = parsePath(req.path);
133
+ const body = JSON.parse(req.body);
134
+ for (const { key: key2, value } of body) {
135
+ api.upsert(name, key2, value);
136
+ }
137
+ return { statusCode: 200 };
138
+ } catch (e) {
139
+ if (e.message === COLLECTION_NOT_FOUND) {
140
+ return { statusCode: 404 };
141
+ }
142
+ }
143
+ };
144
+ const remove2 = (req) => {
145
+ try {
146
+ assertAuth(req);
147
+ } catch (e) {
148
+ return { statusCode: 403 };
149
+ }
150
+ try {
151
+ const { name, key } = parsePath(req.path);
152
+ api.remove(name, key);
153
+ return { statusCode: 200 };
154
+ } catch (e) {
155
+ if (e.message === COLLECTION_NOT_FOUND) {
156
+ return { statusCode: 404 };
157
+ }
158
+ }
159
+ };
160
+ mockPool.intercept({ method: "get", path: /collections\/./ }).reply(get2).persist();
161
+ mockPool.intercept({ method: "post", path: /collections\/./ }).reply(post).persist();
162
+ mockPool.intercept({ method: "delete", path: /collections\/./ }).reply(remove2).persist();
163
+ return {
164
+ api,
165
+ request: ({ method, path, data, ...rest }) => {
166
+ const opts = {
167
+ method,
168
+ path,
169
+ origin: url,
170
+ headers: {
171
+ Authorization: `Bearer abc`
172
+ },
173
+ ...rest
174
+ };
175
+ if (data) {
176
+ Object.assign(opts.headers, {
177
+ "content-type": "application/json"
178
+ });
179
+ opts.body = JSON.stringify(data);
180
+ }
181
+ return mockPool.request(opts);
182
+ }
183
+ };
184
+ }
185
+
186
+ // src/collections.js
187
+ var client;
188
+ var getClient = (state) => {
189
+ var _a;
190
+ if (!client) {
191
+ const baseUrl = ((_a = state.configuration) == null ? void 0 : _a.collections_endpoint) ?? "https://app.openfn.org";
192
+ client = new undici.client(baseUrl);
193
+ }
194
+ return client;
195
+ };
196
+ var setMockClient = (mockClient) => {
197
+ client = mockClient;
198
+ };
199
+ function get(name, query = {}) {
200
+ return async (state) => {
201
+ const [resolvedName, resolvedQuery] = expandReferences(state, name, query);
202
+ const { key } = expandQuery(resolvedQuery);
203
+ const response = await request(
204
+ state,
205
+ getClient(state),
206
+ `${resolvedName}/${key}`
207
+ );
208
+ let data;
209
+ if (!key.match(/\*/) || Object.keys(resolvedQuery).length === 0) {
210
+ [data] = (await response.body.json()).results;
211
+ console.log(`Fetched "${key}" from collection "${name}"`);
212
+ } else {
213
+ data = [];
214
+ console.log(`Downloading data from collection "${name}"...`);
215
+ await streamResponse(response, (item) => {
216
+ data.push(item);
217
+ });
218
+ console.log(`Fetched "${data.length}" values from collection "${name}"`);
219
+ }
220
+ state.data = data;
221
+ return state;
222
+ };
223
+ }
224
+ function set(name, keyGen, values) {
225
+ return async (state) => {
226
+ const [resolvedName, resolvedValues] = expandReferences(
227
+ state,
228
+ name,
229
+ values
230
+ );
231
+ const dataArray = Array.isArray(resolvedValues) ? resolvedValues : [resolvedValues];
232
+ const keyGenFn = typeof keyGen === "string" ? () => keyGen : keyGen;
233
+ const pairs = dataArray.map((value) => ({ key: keyGenFn(value), value }));
234
+ console.log(`Setting ${pairs.length} values in collection "${name}"`);
235
+ const response = await request(state, getClient(state), resolvedName, {
236
+ method: "POST",
237
+ body: JSON.stringify(pairs),
238
+ heeaders: {
239
+ "content-type": "application/json"
240
+ }
241
+ });
242
+ return state;
243
+ };
244
+ }
245
+ function remove(name, query = {}, options = {}) {
246
+ return async (state) => {
247
+ const [resolvedName, resolvedQuery] = expandReferences(state, name, query);
248
+ const { key } = expandQuery(resolvedQuery);
249
+ const response = await request(
250
+ state,
251
+ getClient(state),
252
+ `${resolvedName}/${key}`,
253
+ {
254
+ method: "DELETE"
255
+ }
256
+ );
257
+ return state;
258
+ };
259
+ }
260
+ function each(name, query = {}, callback = {}) {
261
+ return async (state) => {
262
+ const [resolvedName, resolvedQuery] = expandReferences(state, name, query);
263
+ const { key } = expandQuery(resolvedQuery);
264
+ const response = await request(
265
+ state,
266
+ getClient(state),
267
+ `${resolvedName}/${key}`
268
+ );
269
+ await streamResponse(response, async ({ key: key2, value }) => {
270
+ await callback(state, value, key2);
271
+ });
272
+ return state;
273
+ };
274
+ }
275
+ var streamResponse = async (response, onValue) => {
276
+ const pipeline = chain([
277
+ response.body,
278
+ parser(),
279
+ new Pick({ filter: "results" }),
280
+ new streamArray()
281
+ ]);
282
+ for await (const { key, value } of pipeline) {
283
+ await onValue(value);
284
+ }
285
+ };
286
+ var expandQuery = (query) => {
287
+ let key;
288
+ if (typeof query === "string") {
289
+ key = query;
290
+ return {
291
+ key
292
+ };
293
+ }
294
+ return query;
295
+ };
296
+ var request = (state, client2, path, options = {}) => {
297
+ if (!state.configuration.collections_token) {
298
+ throwError("INVALID_AUTH", {
299
+ description: "No access key provided for collection request",
300
+ fix: 'Ensure the "collections_token" value is set on state.configuration',
301
+ path
302
+ });
303
+ }
304
+ const headers = {
305
+ Authorization: `Bearer ${state.configuration.collections_token}`
306
+ };
307
+ Object.assign(headers, options == null ? void 0 : options.headers);
308
+ const { headers: _, ...optionsWithoutHeaders } = options;
309
+ return client2.request({
310
+ path: nodepath.join("collections", path),
311
+ headers,
312
+ method: "GET",
313
+ ...optionsWithoutHeaders
314
+ });
315
+ };
316
+ export {
317
+ collections_exports as collections
318
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@openfn/language-collections",
3
+ "version": "1.0.0-next-13f27ea6",
4
+ "description": "OpenFn collections adaptor",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./types/index.d.ts",
10
+ "require": "./dist/index.cjs"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "author": "Open Function Group",
15
+ "license": "LGPLv3",
16
+ "files": [
17
+ "dist/",
18
+ "types/",
19
+ "ast.json",
20
+ "configuration-schema.json"
21
+ ],
22
+ "dependencies": {
23
+ "stream-chain": "^3.3.2",
24
+ "stream-json": "^1.8.0",
25
+ "undici": "^5.22.1",
26
+ "@openfn/language-common": "2.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "assertion-error": "2.0.0",
30
+ "chai": "4.3.6",
31
+ "deep-eql": "4.1.1",
32
+ "esno": "^0.16.3",
33
+ "rimraf": "3.0.2"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/openfn/adaptors.git"
38
+ },
39
+ "types": "types/index.d.ts",
40
+ "main": "dist/index.cjs",
41
+ "scripts": {
42
+ "tmp": "node test.js",
43
+ "build": "pnpm clean && build-adaptor collections",
44
+ "test": "mocha --experimental-specifier-resolution=node --no-warnings **/*.test.js",
45
+ "test:watch": "mocha -w --experimental-specifier-resolution=node --no-warnings *.test.js",
46
+ "clean": "rimraf dist types docs",
47
+ "pack": "pnpm pack --pack-destination ../../dist",
48
+ "lint": "eslint src"
49
+ }
50
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Query options. All dates should be parseable as ISO 8601 strings, see https://simple.wikipedia.org/wiki/ISO_8601
3
+ * @typedef {Object} QueryOptions
4
+ * @public
5
+ * @property {string} key - key or key pattern to match against. Patterns support wildcards, eg `2024-01*`
6
+ * @property {string} createdBefore - matches values that were created before the start of the provided date
7
+ * @property {string} createdAfter - matches values that were created after the end of the provided date
8
+ * @property {string} updatedBefore - matches values that were updated before the start of the provided date
9
+ * @property {string} updatedAfter - matches values that were updated after the end of the provided date*
10
+ */
11
+ /**
12
+ * Fetch one or more values from a collection.
13
+ * For large datasets, we recommend using each(), which streams data.
14
+ * You can pass a specific key as a string to only fetch one item, or pass a query
15
+ * with a key-pattern or a date filter.
16
+ * @public
17
+ * @function
18
+ * @param {string} name - The name of the collection to fetch from
19
+ * @param {string|QueryOptions} query - A string key or key pattern (with wildcards '*') to fetch, or a query object
20
+ * @state data - the downloaded values as an array unless a specific string was specified
21
+ * @example <caption>Get a specific value from a collection</caption>
22
+ * collections.get('my-collection', '556e0a62')
23
+ * @example <caption>Get a range of values from a collection with a key pattern</caption>
24
+ * collections.get('my-collection', '2024*')
25
+ * @example <caption>Get all values created since the end of January 2024</caption>
26
+ * collections.get('my-collection', { createdAfter: '202401')
27
+ */
28
+ export function get(name: string, query?: string | QueryOptions): (state: any) => Promise<any>;
29
+ /**
30
+ * Adds one or more values to a collection. If a key already exists, its value will
31
+ * be replaced by the new value.
32
+ * You can pass a string key and a single value, or a key generator function and an array of values.
33
+ * The function will be called for each value, passing each value as the first argument, and should return
34
+ * a string key.
35
+ * @public
36
+ * @function
37
+ * @param keygen - a function which generates a key for each value. Pass a string to set a static key for a single item.
38
+ * @param values - an array of values to set, or a single value.
39
+ * @example <caption>Set a number of values using each value's id property as a key</caption>
40
+ * collections.set('my-collection', (item) => item.id, $.data)
41
+ * @example <caption>Set a number of values, generating an id from a string template</caption>
42
+ * collections.set('my-collection', (item) => `${item.category}-${Date.now()}`, $.data)
43
+ * @example <caption>Set a single value with a static key</caption>
44
+ * collections.set('my-collection', 'city-codes', { NY: 'New York', LDN: 'London' }})
45
+ */
46
+ export function set(name: any, keyGen: any, values: any): (state: any) => Promise<any>;
47
+ /**
48
+ * Remove one or more values from a collection.
49
+ * You can pass a specific key as a string to only fetch one item, or pass a query
50
+ * with a key-pattern or a date filter.
51
+ * @public
52
+ * @function
53
+ * @param {string} name - The name of the collection to remove from
54
+ * @param {string|QueryOptions} query - A string key or key pattern (with wildcards '*') to remove, or a query object
55
+ * @example <caption>Remove a specific value from a collection</caption>
56
+ * collections.remove('my-collection', '556e0a62')
57
+ * @example <caption>Remove a range of values from a collection with a key pattern</caption>
58
+ * collections.remove('my-collection', '2024*')
59
+ * @example <caption>Remove all values created since the end of January 2024</caption>
60
+ * collections.remove('my-collection', { createdAfter: '202401')
61
+ */
62
+ export function remove(name: string, query?: string | QueryOptions, options?: {}): (state: any) => Promise<any>;
63
+ /**
64
+ * Iterate over all values in a collection which match the provided query.
65
+ * each() maintains a low memory footprint by streaming items individually.
66
+ * You can pass a string key-pattern as a query, or pass a query object.
67
+ * The callback function will be invoked for each value with three parameters:
68
+ * `state`, `value` and `key`.
69
+ * @public
70
+ * @function
71
+ * @param {string} name - The name of the collection to remove from
72
+ * @param {string|QueryOptions} query - A string key or key pattern (with wildcards '*') to remove, or a query object
73
+ * @param {function} callback - A callback invoked for each item `(state, value, key) => void`
74
+ * @example <caption>Iterate over a range of values with wildcards</caption>
75
+ * collections.each('my-collection', 'record-2024*-appointment-*', (state, value, key) => {
76
+ * state.cumulativeCost += value.cost;
77
+ * })
78
+ * @example <caption>Iterate over a range of values with date filters</caption>
79
+ * collections.each('my-collection', { updatedBefore: new Date().toString() }, (state, value, key) => {
80
+ * state.cumulativeCost += value.cost;
81
+ * })
82
+ */
83
+ export function each(name: string, query?: string | QueryOptions, callback?: Function): (state: any) => Promise<any>;
84
+ export { createServer as createMockServer };
85
+ export function setMockClient(mockClient: any): void;
86
+ export function streamResponse(response: any, onValue: any): Promise<void>;
87
+ export function expandQuery(query: any): any;
88
+ export function request(state: any, client: any, path: any, options?: {}): any;
89
+ /**
90
+ * Query options. All dates should be parseable as ISO 8601 strings, see https://simple.wikipedia.org/wiki/ISO_8601
91
+ */
92
+ export type QueryOptions = any;
93
+ import { createServer } from "./mock";
@@ -0,0 +1 @@
1
+ export * as collections from "./collections";
@@ -0,0 +1,23 @@
1
+ export function API(): {
2
+ collections: {};
3
+ createCollection: (name: any, values?: {}) => void;
4
+ reset: () => void;
5
+ upsert: (name: any, key: any, value: any) => void;
6
+ fetch: (name: any, key: any, query: any) => {
7
+ results: {
8
+ key: string;
9
+ value: any;
10
+ }[];
11
+ };
12
+ remove: (name: any, key: any) => void;
13
+ byKey: (name: any, key: any) => any;
14
+ };
15
+ export function createServer(url?: string): {
16
+ api: any;
17
+ request: ({ method, path, data, ...rest }: {
18
+ [x: string]: any;
19
+ method: any;
20
+ path: any;
21
+ data: any;
22
+ }) => Promise<import("undici/types/dispatcher").default.ResponseData>;
23
+ };