@scarif/scarif-js 1.0.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.
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryBuilderImpl = void 0;
4
+ exports.buildQueryString = buildQueryString;
5
+ const select_parser_1 = require("./select-parser");
6
+ /**
7
+ * Chainable query builder implementation
8
+ */
9
+ class QueryBuilderImpl {
10
+ constructor(table, executeFn) {
11
+ this.table = table;
12
+ this.executeFn = executeFn;
13
+ this.state = {
14
+ filters: {}
15
+ };
16
+ }
17
+ // ===========================================
18
+ // VALIDATION HELPERS
19
+ // ===========================================
20
+ /**
21
+ * Validates that a number is non-negative
22
+ */
23
+ validateNonNegative(value, paramName) {
24
+ if (value < 0) {
25
+ throw new Error(`${paramName} must be non-negative, got ${value}`);
26
+ }
27
+ }
28
+ /**
29
+ * Validates that a value is not null or undefined
30
+ */
31
+ validateNotNull(value, paramName) {
32
+ if (value === null || value === undefined) {
33
+ throw new Error(`${paramName} cannot be null or undefined`);
34
+ }
35
+ }
36
+ /**
37
+ * Validates that an array is not empty
38
+ */
39
+ validateNonEmptyArray(arr, paramName) {
40
+ if (!Array.isArray(arr)) {
41
+ throw new Error(`${paramName} must be an array`);
42
+ }
43
+ if (arr.length === 0) {
44
+ throw new Error(`${paramName} cannot be an empty array`);
45
+ }
46
+ }
47
+ /**
48
+ * Validates column name is a non-empty string
49
+ */
50
+ validateColumnName(column, paramName = 'column') {
51
+ if (typeof column !== 'string' || column.trim().length === 0) {
52
+ throw new Error(`${paramName} must be a non-empty string`);
53
+ }
54
+ }
55
+ // ===========================================
56
+ // SELECTION
57
+ // ===========================================
58
+ select(query) {
59
+ const parsed = (0, select_parser_1.parseSupabaseSelect)(query);
60
+ // Map parsed result to QueryState format
61
+ // Convert tableName to table for ParsedRelation
62
+ this.state.select = {
63
+ primaryColumns: parsed.primaryColumns,
64
+ associatedTables: parsed.associatedTables.map(t => ({
65
+ table: t.tableName,
66
+ columns: t.columns
67
+ }))
68
+ };
69
+ return this;
70
+ }
71
+ // ===========================================
72
+ // FILTERS
73
+ // ===========================================
74
+ /**
75
+ * Helper method to add object-based filters (eq, neq, gt, etc.)
76
+ */
77
+ addObjectFilter(key, column, value) {
78
+ this.validateColumnName(column);
79
+ this.validateNotNull(value, `Filter value for ${column}`);
80
+ if (!this.state.filters[key]) {
81
+ this.state.filters[key] = {};
82
+ }
83
+ this.state.filters[key][column] = value;
84
+ return this;
85
+ }
86
+ /**
87
+ * Helper method to add array-based filters (in, notIn)
88
+ */
89
+ addArrayFilter(key, column, values) {
90
+ this.validateColumnName(column);
91
+ this.validateNonEmptyArray(values, `Filter values for ${column}`);
92
+ if (!this.state.filters[key]) {
93
+ this.state.filters[key] = {};
94
+ }
95
+ this.state.filters[key][column] = values.map(String);
96
+ return this;
97
+ }
98
+ /**
99
+ * Helper method to add column list filters (isNull, isNotNull)
100
+ */
101
+ addColumnListFilter(key, column) {
102
+ this.validateColumnName(column);
103
+ if (!this.state.filters[key]) {
104
+ this.state.filters[key] = [];
105
+ }
106
+ this.state.filters[key].push(column);
107
+ return this;
108
+ }
109
+ eq(column, value) {
110
+ return this.addObjectFilter('eq', column, value);
111
+ }
112
+ neq(column, value) {
113
+ return this.addObjectFilter('neq', column, value);
114
+ }
115
+ gt(column, value) {
116
+ return this.addObjectFilter('gt', column, value);
117
+ }
118
+ gte(column, value) {
119
+ return this.addObjectFilter('gte', column, value);
120
+ }
121
+ lt(column, value) {
122
+ return this.addObjectFilter('lt', column, value);
123
+ }
124
+ lte(column, value) {
125
+ return this.addObjectFilter('lte', column, value);
126
+ }
127
+ like(column, value) {
128
+ return this.addObjectFilter('like', column, value);
129
+ }
130
+ ilike(column, value) {
131
+ return this.addObjectFilter('ilike', column, value);
132
+ }
133
+ in(column, values) {
134
+ return this.addArrayFilter('in', column, values);
135
+ }
136
+ notIn(column, values) {
137
+ return this.addArrayFilter('notIn', column, values);
138
+ }
139
+ isNull(column) {
140
+ return this.addColumnListFilter('isNull', column);
141
+ }
142
+ isNotNull(column) {
143
+ return this.addColumnListFilter('isNotNull', column);
144
+ }
145
+ // ===========================================
146
+ // ORDERING
147
+ // ===========================================
148
+ order(column, direction = 'asc') {
149
+ this.validateColumnName(column, 'order column');
150
+ if (!this.state.order)
151
+ this.state.order = [];
152
+ this.state.order.push({ column, direction });
153
+ return this;
154
+ }
155
+ // ===========================================
156
+ // PAGINATION
157
+ // ===========================================
158
+ limit(count) {
159
+ this.validateNonNegative(count, 'limit');
160
+ this.state.limit = count;
161
+ return this;
162
+ }
163
+ offset(count) {
164
+ this.validateNonNegative(count, 'offset');
165
+ this.state.offset = count;
166
+ return this;
167
+ }
168
+ range(from, to) {
169
+ this.validateNonNegative(from, 'range start');
170
+ this.validateNonNegative(to, 'range end');
171
+ if (to < from) {
172
+ throw new Error(`range end (${to}) must be >= range start (${from})`);
173
+ }
174
+ this.state.offset = from;
175
+ // Calculate limit: to is exclusive (like array.slice), so limit = to - from
176
+ this.state.limit = to - from;
177
+ return this;
178
+ }
179
+ // ===========================================
180
+ // RESPONSE MODIFIERS
181
+ // ===========================================
182
+ single() {
183
+ this.state.single = true;
184
+ // Only set limit if not already set to avoid overwriting user's limit()
185
+ if (this.state.limit === undefined) {
186
+ this.state.limit = 1;
187
+ }
188
+ return this;
189
+ }
190
+ // ===========================================
191
+ // THENABLE INTERFACE
192
+ // ===========================================
193
+ then(onfulfilled, onrejected) {
194
+ // Enforce required select()
195
+ if (!this.state.select) {
196
+ const error = new Error("select() is required. Use .select('*') to select all columns.");
197
+ return Promise.reject(error);
198
+ }
199
+ // Create the base promise from executeFn
200
+ const basePromise = this.executeFn(this.table, this.state);
201
+ // Transform the response based on single flag
202
+ const transformedPromise = basePromise.then(response => {
203
+ if (this.state.single) {
204
+ // Handle single result
205
+ if (response.error) {
206
+ return response;
207
+ }
208
+ const data = Array.isArray(response.data) && response.data.length > 0
209
+ ? response.data[0]
210
+ : null;
211
+ return {
212
+ ...response,
213
+ data
214
+ };
215
+ }
216
+ return response;
217
+ });
218
+ // Always chain the handlers (Promise.then works with undefined handlers)
219
+ return transformedPromise.then(onfulfilled, onrejected);
220
+ }
221
+ catch(onrejected) {
222
+ return this.then().catch(onrejected);
223
+ }
224
+ }
225
+ exports.QueryBuilderImpl = QueryBuilderImpl;
226
+ // ===========================================
227
+ // QUERY STRING BUILDER
228
+ // ===========================================
229
+ /**
230
+ * Helper function to add object-based filter parameters to URLSearchParams
231
+ */
232
+ function addObjectFilterParams(params, filter, filterType) {
233
+ if (filter) {
234
+ for (const [key, value] of Object.entries(filter)) {
235
+ params.append(`${key}[${filterType}]`, String(value));
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Helper function to add array-based filter parameters to URLSearchParams
241
+ */
242
+ function addArrayFilterParams(params, filter, filterType) {
243
+ if (filter) {
244
+ for (const [key, values] of Object.entries(filter)) {
245
+ params.append(`${key}[${filterType}]`, values.join(','));
246
+ }
247
+ }
248
+ }
249
+ /**
250
+ * Helper function to add column list filter parameters to URLSearchParams
251
+ */
252
+ function addColumnListFilterParams(params, columns, filterType) {
253
+ if (columns) {
254
+ for (const column of columns) {
255
+ params.append(`${column}[${filterType}]`, 'true');
256
+ }
257
+ }
258
+ }
259
+ /**
260
+ * Builds URL query string from QueryState
261
+ *
262
+ * Transforms the internal QueryState structure into URL query parameters
263
+ * following the API's expected format:
264
+ * - Select columns: `select=col1,col2`
265
+ * - Relations: `with=table(col1,col2)`
266
+ * - Filters: `column[filterType]=value`
267
+ * - Ordering: `order=column.direction`
268
+ * - Pagination: `limit=N&offset=N`
269
+ *
270
+ * @param state - The query state containing all filters, selections, and modifiers
271
+ * @returns Query string with leading '?' if params exist, empty string otherwise
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const state: QueryState = {
276
+ * select: { primaryColumns: ['id', 'name'], associatedTables: [] },
277
+ * filters: { eq: { status: 'active' } },
278
+ * limit: 10
279
+ * };
280
+ * buildQueryString(state); // "?select=id,name&status[eq]=active&limit=10"
281
+ * ```
282
+ */
283
+ function buildQueryString(state) {
284
+ const params = new URLSearchParams();
285
+ // Select columns and relations (transformed from parsed select)
286
+ if (state.select) {
287
+ // Primary columns
288
+ if (state.select.primaryColumns.length > 0) {
289
+ params.append('select', state.select.primaryColumns.join(','));
290
+ }
291
+ // Associated tables as 'with' parameters
292
+ if (state.select.associatedTables.length > 0) {
293
+ for (const relation of state.select.associatedTables) {
294
+ if (relation.columns && relation.columns.length > 0) {
295
+ params.append('with', `${relation.table}(${relation.columns.join(',')})`);
296
+ }
297
+ else {
298
+ params.append('with', relation.table);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ // Filters - object-based filters
304
+ const { filters } = state;
305
+ addObjectFilterParams(params, filters.eq, 'eq');
306
+ addObjectFilterParams(params, filters.neq, 'neq');
307
+ addObjectFilterParams(params, filters.gt, 'gt');
308
+ addObjectFilterParams(params, filters.gte, 'gte');
309
+ addObjectFilterParams(params, filters.lt, 'lt');
310
+ addObjectFilterParams(params, filters.lte, 'lte');
311
+ addObjectFilterParams(params, filters.like, 'like');
312
+ addObjectFilterParams(params, filters.ilike, 'ilike');
313
+ // Filters - array-based filters
314
+ addArrayFilterParams(params, filters.in, 'in');
315
+ addArrayFilterParams(params, filters.notIn, 'notIn');
316
+ // Filters - column list filters
317
+ addColumnListFilterParams(params, filters.isNull, 'isNull');
318
+ addColumnListFilterParams(params, filters.isNotNull, 'isNotNull');
319
+ // Order
320
+ if (state.order && state.order.length > 0) {
321
+ const orderStr = state.order
322
+ .map(o => `${o.column}.${o.direction}`)
323
+ .join(',');
324
+ params.append('order', orderStr);
325
+ }
326
+ // Limit
327
+ if (state.limit !== undefined) {
328
+ params.append('limit', String(state.limit));
329
+ }
330
+ // Offset
331
+ if (state.offset !== undefined) {
332
+ params.append('offset', String(state.offset));
333
+ }
334
+ const queryString = params.toString();
335
+ return queryString ? `?${queryString}` : '';
336
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Result of parsing a Supabase-style select query string
3
+ */
4
+ export interface ParsedQuery {
5
+ primaryColumns: string[];
6
+ associatedTables: {
7
+ tableName: string;
8
+ columns: string[];
9
+ }[];
10
+ }
11
+ /**
12
+ * Parses a Supabase/PostgREST-style select query string into structured data
13
+ *
14
+ * @param selectQuery - Select query string (e.g., "id, name, posts(title, content)")
15
+ * @returns Parsed query with primary columns and associated tables
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * parseSupabaseSelect('id, name, posts(title, content)')
20
+ * // Returns:
21
+ * // {
22
+ * // primaryColumns: ['id', 'name'],
23
+ * // associatedTables: [{ tableName: 'posts', columns: ['title', 'content'] }]
24
+ * // }
25
+ * ```
26
+ */
27
+ export declare function parseSupabaseSelect(selectQuery: string): ParsedQuery;
28
+ //# sourceMappingURL=select-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select-parser.d.ts","sourceRoot":"","sources":["../../src/utils/select-parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,EAAE,CAAC;CACL;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAqEpE"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSupabaseSelect = parseSupabaseSelect;
4
+ /**
5
+ * Parses a Supabase/PostgREST-style select query string into structured data
6
+ *
7
+ * @param selectQuery - Select query string (e.g., "id, name, posts(title, content)")
8
+ * @returns Parsed query with primary columns and associated tables
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * parseSupabaseSelect('id, name, posts(title, content)')
13
+ * // Returns:
14
+ * // {
15
+ * // primaryColumns: ['id', 'name'],
16
+ * // associatedTables: [{ tableName: 'posts', columns: ['title', 'content'] }]
17
+ * // }
18
+ * ```
19
+ */
20
+ function parseSupabaseSelect(selectQuery) {
21
+ // Remove extra whitespace and newlines
22
+ const normalized = selectQuery.replace(/\s+/g, ' ').trim();
23
+ const result = {
24
+ primaryColumns: [],
25
+ associatedTables: []
26
+ };
27
+ let i = 0;
28
+ let currentToken = '';
29
+ let parenDepth = 0;
30
+ while (i < normalized.length) {
31
+ const char = normalized[i];
32
+ if (char === '(') {
33
+ parenDepth++;
34
+ if (parenDepth === 1 && currentToken.trim()) {
35
+ // Start of associated table
36
+ const tableName = currentToken.trim();
37
+ i++; // Skip opening paren
38
+ // Extract content within parentheses
39
+ let tableContent = '';
40
+ let depth = 1;
41
+ while (i < normalized.length && depth > 0) {
42
+ if (normalized[i] === '(')
43
+ depth++;
44
+ if (normalized[i] === ')')
45
+ depth--;
46
+ if (depth > 0)
47
+ tableContent += normalized[i];
48
+ i++;
49
+ }
50
+ // Parse columns for this table
51
+ const columns = tableContent
52
+ .split(',')
53
+ .map(col => col.trim())
54
+ .filter(col => col.length > 0);
55
+ result.associatedTables.push({
56
+ tableName,
57
+ columns
58
+ });
59
+ currentToken = '';
60
+ parenDepth = 0;
61
+ continue;
62
+ }
63
+ }
64
+ else if (char === ',') {
65
+ if (parenDepth === 0 && currentToken.trim()) {
66
+ // Primary column
67
+ result.primaryColumns.push(currentToken.trim());
68
+ currentToken = '';
69
+ }
70
+ else if (parenDepth > 0) {
71
+ currentToken += char;
72
+ }
73
+ }
74
+ else {
75
+ currentToken += char;
76
+ }
77
+ i++;
78
+ }
79
+ // Add last token if it exists and we're not in parentheses
80
+ if (currentToken.trim() && parenDepth === 0) {
81
+ result.primaryColumns.push(currentToken.trim());
82
+ }
83
+ return result;
84
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@scarif/scarif-js",
3
+ "version": "1.0.0",
4
+ "description": "A TypeScript SDK for Scarif API services",
5
+ "keywords": [
6
+ "\"api",
7
+ "sdk",
8
+ "data\""
9
+ ],
10
+ "homepage": "https://github.com/GeneralTyres/stoorplek-sdk#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/GeneralTyres/stoorplek-sdk/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/GeneralTyres/stoorplek-sdk.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "GeneralTyres",
20
+ "type": "commonjs",
21
+ "main": "dist/index.js",
22
+ "types": "dist/index.d.ts",
23
+ "directories": {
24
+ "doc": "docs"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "prepare": "npm run build",
33
+ "test": "echo \"No tests yet\"",
34
+ "dev": "tsc --watch",
35
+ "type-check": "tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "undici-types": "^7.16.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.0.7",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }