@genrojs/bag 0.10.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.
@@ -0,0 +1,209 @@
1
+ // Copyright 2025 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ /**
4
+ * UrlResolver - resolver that loads content from an HTTP URL.
5
+ *
6
+ * Uses fetchTytx from @genrojs/tytx for typed serialization/deserialization.
7
+ * Faithful port of Python genro_bag.resolvers.url_resolver.UrlResolver.
8
+ */
9
+
10
+ import { fetchTytx } from '@genrojs/tytx';
11
+ import { BagResolver } from '../resolver.js';
12
+
13
+ /**
14
+ * Resolver that fetches content from an HTTP URL.
15
+ *
16
+ * Supports all HTTP methods and can convert responses to Bag automatically.
17
+ * Uses fetchTytx for TYTX-aware HTTP operations.
18
+ *
19
+ * @example
20
+ * // Simple GET
21
+ * const resolver = new UrlResolver('https://api.example.com/data');
22
+ * const data = await resolver.resolve();
23
+ *
24
+ * @example
25
+ * // GET with query params, parse as Bag
26
+ * const resolver = new UrlResolver('https://api.example.com/users', {
27
+ * qs: { page: 1, limit: 10 },
28
+ * asBag: true
29
+ * });
30
+ * const users = await resolver.resolve();
31
+ *
32
+ * @example
33
+ * // POST with body
34
+ * const resolver = new UrlResolver('https://api.example.com/users', {
35
+ * method: 'post',
36
+ * body: { name: 'John', email: 'john@example.com' },
37
+ * asBag: true
38
+ * });
39
+ */
40
+ export class UrlResolver extends BagResolver {
41
+ static classKwargs = {
42
+ cacheTime: 300,
43
+ readOnly: true,
44
+ retryPolicy: {
45
+ maxAttempts: 3,
46
+ delay: 1.0,
47
+ backoff: 2.0,
48
+ jitter: true,
49
+ on: ['TypeError', 'NetworkError', 'AbortError']
50
+ },
51
+ asBag: false,
52
+ url: null,
53
+ method: 'get',
54
+ qs: null,
55
+ body: null,
56
+ timeout: 5, // seconds (same unit as Python)
57
+ transport: 'json'
58
+ };
59
+
60
+ static classArgs = ['url'];
61
+
62
+ static internalParams = new Set([
63
+ 'cacheTime', 'readOnly', 'retryPolicy', 'asBag',
64
+ 'url', 'method', 'qs', 'body', 'timeout', 'transport'
65
+ ]);
66
+
67
+ /**
68
+ * Create a URL resolver.
69
+ *
70
+ * @param {string|Object} urlOrKwargs - URL string or options object.
71
+ * @param {Object} [kwargs] - Additional options if first arg is URL.
72
+ */
73
+ constructor(urlOrKwargs, kwargs = {}) {
74
+ // Allow positional URL: new UrlResolver('http://...')
75
+ if (typeof urlOrKwargs === 'string') {
76
+ kwargs = { ...kwargs, url: urlOrKwargs };
77
+ } else {
78
+ kwargs = urlOrKwargs || {};
79
+ }
80
+ super(kwargs);
81
+ }
82
+
83
+ /**
84
+ * Load data from URL.
85
+ *
86
+ * @param {Object} kwargs - Parameters for this load call.
87
+ * @returns {Promise<*>} Response data.
88
+ */
89
+ async load(kwargs) {
90
+ let url = this._kw.url;
91
+ const method = this._kw.method;
92
+ const qs = this._kw.qs;
93
+ const timeout = this._kw.timeout;
94
+ const transport = this._kw.transport;
95
+ let body = this._kw.body;
96
+
97
+ // _body overrides constructor body
98
+ if (kwargs._body !== undefined) {
99
+ body = kwargs._body;
100
+ }
101
+
102
+ // Collect path args (arg_0, arg_1, ...) and extra qs params
103
+ const pathArgs = [];
104
+ const extraQs = {};
105
+
106
+ for (const [key, value] of Object.entries(kwargs)) {
107
+ if (key.startsWith('arg_') && value !== null && value !== undefined) {
108
+ const idx = parseInt(key.slice(4), 10);
109
+ if (!isNaN(idx)) {
110
+ while (pathArgs.length <= idx) {
111
+ pathArgs.push(null);
112
+ }
113
+ pathArgs[idx] = value;
114
+ }
115
+ } else if (
116
+ !this.constructor.classKwargs.hasOwnProperty(key) &&
117
+ !this.constructor.internalParams.has(key) &&
118
+ !key.startsWith('_')
119
+ ) {
120
+ // Extra kwarg -> query string parameter
121
+ if (value !== null && value !== undefined) {
122
+ extraQs[key] = value;
123
+ }
124
+ }
125
+ }
126
+
127
+ // Substitute path parameters {placeholder} with arg_0, arg_1, ...
128
+ if (pathArgs.length > 0 && url.includes('{')) {
129
+ const placeholders = url.match(/\{([^}]+)\}/g) || [];
130
+ placeholders.forEach((placeholder, i) => {
131
+ if (i < pathArgs.length && pathArgs[i] !== null) {
132
+ url = url.replace(placeholder, String(pathArgs[i]));
133
+ }
134
+ });
135
+ }
136
+
137
+ // Merge query string: constructor qs + extra kwargs
138
+ const mergedQs = {};
139
+ if (qs) {
140
+ Object.assign(mergedQs, this._qsToDict(qs));
141
+ }
142
+ Object.assign(mergedQs, extraQs);
143
+
144
+ // Build URL with query string (filter null/undefined values)
145
+ if (Object.keys(mergedQs).length > 0) {
146
+ const params = new URLSearchParams();
147
+ for (const [key, value] of Object.entries(mergedQs)) {
148
+ if (value !== null && value !== undefined) {
149
+ params.append(key, String(value));
150
+ }
151
+ }
152
+ const separator = url.includes('?') ? '&' : '?';
153
+ url = `${url}${separator}${params.toString()}`;
154
+ }
155
+
156
+ // Build fetch options
157
+ const fetchOptions = {
158
+ method: method.toUpperCase(),
159
+ transport,
160
+ };
161
+
162
+ // Add timeout via AbortController
163
+ if (timeout) {
164
+ fetchOptions.signal = AbortSignal.timeout(timeout * 1000);
165
+ }
166
+
167
+ // Add body for POST/PUT/PATCH
168
+ if (body !== null && body !== undefined) {
169
+ fetchOptions.body = body;
170
+ }
171
+
172
+ // Make request using fetchTytx
173
+ const response = await fetchTytx(url, fetchOptions);
174
+
175
+ return response;
176
+ }
177
+
178
+ /**
179
+ * Convert query string source to dict, filtering null/undefined values.
180
+ *
181
+ * @param {Object|Map} qs - Query string parameters.
182
+ * @returns {Object} Parameters with null/undefined values removed.
183
+ * @private
184
+ */
185
+ _qsToDict(qs) {
186
+ if (!qs) return {};
187
+
188
+ // Handle Bag-like objects with keys() method
189
+ if (typeof qs.keys === 'function' && typeof qs.getItem === 'function') {
190
+ const result = {};
191
+ for (const k of qs.keys()) {
192
+ const v = qs.getItem(k);
193
+ if (v !== null && v !== undefined) {
194
+ result[k] = v;
195
+ }
196
+ }
197
+ return result;
198
+ }
199
+
200
+ // Handle plain objects
201
+ const result = {};
202
+ for (const [k, v] of Object.entries(qs)) {
203
+ if (v !== null && v !== undefined) {
204
+ result[k] = v;
205
+ }
206
+ }
207
+ return result;
208
+ }
209
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright 2025 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ import { BagResolver } from '../resolver.js';
4
+
5
+
6
+ /**
7
+ * UuidResolver - resolver that generates UUIDs.
8
+ *
9
+ * Uses crypto.globalThis.crypto.randomUUID() (Node 18+ and modern browsers).
10
+ *
11
+ * // DIFF-PYTHON: Python supports uuid1 and uuid4 via the version parameter.
12
+ * // JS only supports uuid4 via crypto.globalThis.crypto.randomUUID(). The version parameter
13
+ * // is accepted for API compatibility but only 'uuid4' is implemented.
14
+ *
15
+ * @example
16
+ * const resolver = new UuidResolver();
17
+ * const uuid = resolver.resolve(); // e.g. '550e8400-e29b-41d4-a716-446655440000'
18
+ */
19
+ export class UuidResolver extends BagResolver {
20
+ static classKwargs = {
21
+ cacheTime: -1,
22
+ readOnly: false,
23
+ asBag: false,
24
+ retryPolicy: null,
25
+ version: 'uuid4'
26
+ };
27
+
28
+ static classArgs = ['version'];
29
+
30
+ static internalParams = new Set([
31
+ 'cacheTime', 'readOnly', 'asBag', 'retryPolicy', 'version'
32
+ ]);
33
+
34
+ load() {
35
+ return globalThis.crypto.randomUUID();
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ // Copyright 2025 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ /**
4
+ * Specialized resolvers for genro-bag.
5
+ */
6
+
7
+ export { UrlResolver } from './UrlResolver.js';
8
+ export { UuidResolver } from './UuidResolver.js';
9
+ export { StorageResolver } from './StorageResolver.js';
@@ -0,0 +1,61 @@
1
+ // Copyright 2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ import { getRegisteredType, getSubtypeDict } from '@genrojs/tytx';
4
+ import { BagSerializationError } from './resolver-wire.js';
5
+
6
+ /**
7
+ * Wire encoding of Bag subclasses: the __cls name, shared by toTytx and fromTytx.
8
+ *
9
+ * A Bag branch travels as "::<suffix>" ("::X" for every Bag subclass that does
10
+ * not declare a suffix of its own). Which class of that type it is travels as a
11
+ * symbolic name, looked up in the TYTX subtype dictionary of the suffix
12
+ * (getSubtypeDict): name -> class, the real class name, identical in Python.
13
+ *
14
+ * A branch carries the name in the attributes of its row; a root (the payload
15
+ * of toTytx, including a Bag held in an attribute or a plain container value)
16
+ * carries it at payload level, next to rows. It is written only when the class
17
+ * differs from the inherited one: a branch inherits the class of its parent
18
+ * Bag when both share the suffix, otherwise the class registered for the
19
+ * suffix; a root inherits the class registered for the suffix.
20
+ *
21
+ * Every class that travels this way is in the dictionary, the base class
22
+ * included (genro-bag-js adds "Bag"). __cls is reserved: a user attribute with
23
+ * that name is an error.
24
+ */
25
+ export const CLS_ATTRIBUTE = '__cls';
26
+
27
+ /** The class a branch (or, with parentClass null, a root) has without __cls. */
28
+ export function getInheritedClass(parentClass, suffix) {
29
+ if (parentClass !== null && parentClass.tytxSuffix === suffix) return parentClass;
30
+ return getRegisteredType(suffix);
31
+ }
32
+
33
+ /** The symbolic name of a Bag class in the subtype dictionary of its suffix. */
34
+ export function getSubtypeName(cls) {
35
+ const suffix = cls.tytxSuffix;
36
+ const names = Object.entries(getSubtypeDict(suffix))
37
+ .filter(([, subtype]) => subtype === cls).map(([name]) => name);
38
+ if (names.length === 0) {
39
+ throw new BagSerializationError(
40
+ `${cls.name} is not in the TYTX subtype dictionary of '${suffix}'`);
41
+ }
42
+ if (names.length > 1) {
43
+ throw new BagSerializationError(
44
+ `${cls.name} is in the TYTX subtype dictionary of '${suffix}' under several names: ${names.sort().join(', ')}`);
45
+ }
46
+ return names[0];
47
+ }
48
+
49
+ /** The Bag class registered under a symbolic name for a suffix. */
50
+ export function getSubtypeClass(suffix, name) {
51
+ const subtypes = getSubtypeDict(suffix);
52
+ if (!Object.hasOwn(subtypes, name)) {
53
+ throw new BagSerializationError(`Unknown Bag class '${name}' for TYTX type '${suffix}'`);
54
+ }
55
+ return subtypes[name];
56
+ }
57
+
58
+ /** The __cls value to write for cls, or null when it is the inherited class. */
59
+ export function getClsMarker(cls, inherited) {
60
+ return cls === inherited ? null : getSubtypeName(cls);
61
+ }