openbase-js 0.1.2 → 0.1.4
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/index.cjs +251 -0
- package/index.d.ts +10 -0
- package/index.js +113 -14
- package/package.json +1 -1
package/index.cjs
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// ─── Query Builder ────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
class QueryBuilder {
|
|
4
|
+
constructor(baseUrl, apiKey, dbName, table) {
|
|
5
|
+
this._baseUrl = baseUrl;
|
|
6
|
+
this._apiKey = apiKey;
|
|
7
|
+
this._dbName = dbName;
|
|
8
|
+
this._table = table;
|
|
9
|
+
this._filters = []; // changed: was {} now array of {col, op, val}
|
|
10
|
+
this._columns = '*';
|
|
11
|
+
this._limitVal = null;
|
|
12
|
+
this._offsetVal = null; // new
|
|
13
|
+
this._orderCol = null; // new
|
|
14
|
+
this._orderAsc = true; // new
|
|
15
|
+
this._single = false;
|
|
16
|
+
this._operation = 'select';
|
|
17
|
+
this._insertData = null;
|
|
18
|
+
this._updateData = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
select(columns = '*') {
|
|
22
|
+
this._columns = columns;
|
|
23
|
+
this._operation = 'select';
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Filter operators ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
eq(column, value) {
|
|
30
|
+
this._filters.push({ col: column, op: 'eq', val: value });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gt(column, value) {
|
|
35
|
+
this._filters.push({ col: column, op: 'gt', val: value });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lt(column, value) {
|
|
40
|
+
this._filters.push({ col: column, op: 'lt', val: value });
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
gte(column, value) {
|
|
45
|
+
this._filters.push({ col: column, op: 'gte', val: value });
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lte(column, value) {
|
|
50
|
+
this._filters.push({ col: column, op: 'lte', val: value });
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
like(column, pattern) {
|
|
55
|
+
this._filters.push({ col: column, op: 'like', val: pattern });
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ilike(column, pattern) {
|
|
60
|
+
this._filters.push({ col: column, op: 'ilike', val: pattern });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
in(column, values) {
|
|
65
|
+
this._filters.push({ col: column, op: 'in', val: values });
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
is(column, value) {
|
|
70
|
+
// value should be null, true, or false
|
|
71
|
+
this._filters.push({ col: column, op: 'is', val: value });
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Modifiers ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
order(column, { ascending = true } = {}) {
|
|
78
|
+
this._orderCol = column;
|
|
79
|
+
this._orderAsc = ascending;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
limit(n) {
|
|
84
|
+
this._limitVal = n;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
range(from, to) {
|
|
89
|
+
// e.g. range(0, 9) => LIMIT 10 OFFSET 0
|
|
90
|
+
this._limitVal = to - from + 1;
|
|
91
|
+
this._offsetVal = from;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
single() {
|
|
96
|
+
this._single = true;
|
|
97
|
+
this._limitVal = 1;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Mutation operators ────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
insert(data) {
|
|
104
|
+
this._operation = 'insert';
|
|
105
|
+
this._insertData = data;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
update(data) {
|
|
110
|
+
this._operation = 'update';
|
|
111
|
+
this._updateData = data;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delete() {
|
|
116
|
+
this._operation = 'delete';
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── SQL Builder ───────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
_filterToSQL(f) {
|
|
123
|
+
const col = `"${f.col}"`;
|
|
124
|
+
switch (f.op) {
|
|
125
|
+
case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
|
|
126
|
+
case 'gt': return `${col} > ${f.val}`;
|
|
127
|
+
case 'lt': return `${col} < ${f.val}`;
|
|
128
|
+
case 'gte': return `${col} >= ${f.val}`;
|
|
129
|
+
case 'lte': return `${col} <= ${f.val}`;
|
|
130
|
+
case 'like': return `${col} LIKE '${f.val}'`;
|
|
131
|
+
case 'ilike': return `${col} ILIKE '${f.val}'`;
|
|
132
|
+
case 'in': {
|
|
133
|
+
const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
|
|
134
|
+
return `${col} IN (${list})`;
|
|
135
|
+
}
|
|
136
|
+
case 'is': {
|
|
137
|
+
if (f.val === null) return `${col} IS NULL`;
|
|
138
|
+
if (f.val === true) return `${col} IS TRUE`;
|
|
139
|
+
if (f.val === false) return `${col} IS FALSE`;
|
|
140
|
+
return `${col} IS NULL`;
|
|
141
|
+
}
|
|
142
|
+
default: return `${col} = '${f.val}'`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_buildSQL() {
|
|
147
|
+
let sql = '';
|
|
148
|
+
|
|
149
|
+
if (this._operation === 'select') {
|
|
150
|
+
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
151
|
+
|
|
152
|
+
if (this._filters.length) {
|
|
153
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
154
|
+
sql += ` WHERE ${where}`;
|
|
155
|
+
}
|
|
156
|
+
if (this._orderCol) {
|
|
157
|
+
sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
|
|
158
|
+
}
|
|
159
|
+
if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
|
|
160
|
+
if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
|
|
161
|
+
|
|
162
|
+
} else if (this._operation === 'insert') {
|
|
163
|
+
const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
|
|
164
|
+
const vals = Object.values(this._insertData).map(v =>
|
|
165
|
+
typeof v === 'string' ? `'${v.replace(/'/g, "''")}'` : v
|
|
166
|
+
).join(', ');
|
|
167
|
+
sql = `INSERT INTO "${this._table}" (${cols}) VALUES (${vals}) RETURNING *`;
|
|
168
|
+
|
|
169
|
+
} else if (this._operation === 'update') {
|
|
170
|
+
const setClauses = Object.entries(this._updateData).map(([col, val]) =>
|
|
171
|
+
typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
|
|
172
|
+
).join(', ');
|
|
173
|
+
sql = `UPDATE "${this._table}" SET ${setClauses}`;
|
|
174
|
+
if (this._filters.length) {
|
|
175
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
176
|
+
sql += ` WHERE ${where}`;
|
|
177
|
+
}
|
|
178
|
+
sql += ` RETURNING *`;
|
|
179
|
+
|
|
180
|
+
} else if (this._operation === 'delete') {
|
|
181
|
+
sql = `DELETE FROM "${this._table}"`;
|
|
182
|
+
if (this._filters.length) {
|
|
183
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
184
|
+
sql += ` WHERE ${where}`;
|
|
185
|
+
}
|
|
186
|
+
sql += ` RETURNING *`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return sql;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Executor ──────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async _buildAndRun() {
|
|
195
|
+
const sql = this._buildSQL();
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(`${this._baseUrl}/query`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
'Authorization': `Bearer ${this._apiKey}`,
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({ sql, db_name: this._dbName }),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const json = await res.json();
|
|
207
|
+
const { data, error } = json;
|
|
208
|
+
if (error) return { data: null, error };
|
|
209
|
+
if (this._single) return { data: data[0] || null, error: null };
|
|
210
|
+
return { data, error: null };
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return { data: null, error: err.message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
then(resolve, reject) {
|
|
217
|
+
return this._buildAndRun().then(resolve, reject);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
class OpenbaseClient {
|
|
224
|
+
constructor(baseUrl, apiKey, dbName) {
|
|
225
|
+
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
226
|
+
this._apiKey = apiKey;
|
|
227
|
+
this._dbName = dbName;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
from(table) {
|
|
231
|
+
return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function createClient(baseUrl, apiKey, dbName) {
|
|
238
|
+
if (!baseUrl) throw new Error('[openbase] Missing baseUrl. Pass your backend URL as the first argument.');
|
|
239
|
+
if (!apiKey) throw new Error('[openbase] Missing apiKey. Pass your anon or service key as the second argument.');
|
|
240
|
+
if (!dbName) throw new Error('[openbase] Missing dbName. Pass your database name as the third argument.');
|
|
241
|
+
return new OpenbaseClient(baseUrl, apiKey, dbName);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typeof module !== 'undefined') {
|
|
245
|
+
module.exports = { createClient };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof window !== 'undefined') {
|
|
249
|
+
window.openbase = { createClient };
|
|
250
|
+
}
|
|
251
|
+
module.exports = { createClient };
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ export interface OpenbaseResponse<T> {
|
|
|
6
6
|
export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
7
7
|
select(columns?: string): this;
|
|
8
8
|
eq(column: string, value: unknown): this;
|
|
9
|
+
gt(column: string, value: number): this;
|
|
10
|
+
lt(column: string, value: number): this;
|
|
11
|
+
gte(column: string, value: number): this;
|
|
12
|
+
lte(column: string, value: number): this;
|
|
13
|
+
like(column: string, pattern: string): this;
|
|
14
|
+
ilike(column: string, pattern: string): this;
|
|
15
|
+
in(column: string, values: unknown[]): this;
|
|
16
|
+
is(column: string, value: null | boolean): this;
|
|
17
|
+
order(column: string, options?: { ascending?: boolean }): this;
|
|
18
|
+
range(from: number, to: number): this;
|
|
9
19
|
limit(n: number): this;
|
|
10
20
|
single(): this;
|
|
11
21
|
insert(data: Partial<T>): this;
|
package/index.js
CHANGED
|
@@ -6,9 +6,12 @@ class QueryBuilder {
|
|
|
6
6
|
this._apiKey = apiKey;
|
|
7
7
|
this._dbName = dbName;
|
|
8
8
|
this._table = table;
|
|
9
|
-
this._filters = {}
|
|
9
|
+
this._filters = []; // changed: was {} now array of {col, op, val}
|
|
10
10
|
this._columns = '*';
|
|
11
11
|
this._limitVal = null;
|
|
12
|
+
this._offsetVal = null; // new
|
|
13
|
+
this._orderCol = null; // new
|
|
14
|
+
this._orderAsc = true; // new
|
|
12
15
|
this._single = false;
|
|
13
16
|
this._operation = 'select';
|
|
14
17
|
this._insertData = null;
|
|
@@ -21,8 +24,59 @@ class QueryBuilder {
|
|
|
21
24
|
return this;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
// ─── Filter operators ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
24
29
|
eq(column, value) {
|
|
25
|
-
this._filters
|
|
30
|
+
this._filters.push({ col: column, op: 'eq', val: value });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gt(column, value) {
|
|
35
|
+
this._filters.push({ col: column, op: 'gt', val: value });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lt(column, value) {
|
|
40
|
+
this._filters.push({ col: column, op: 'lt', val: value });
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
gte(column, value) {
|
|
45
|
+
this._filters.push({ col: column, op: 'gte', val: value });
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lte(column, value) {
|
|
50
|
+
this._filters.push({ col: column, op: 'lte', val: value });
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
like(column, pattern) {
|
|
55
|
+
this._filters.push({ col: column, op: 'like', val: pattern });
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ilike(column, pattern) {
|
|
60
|
+
this._filters.push({ col: column, op: 'ilike', val: pattern });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
in(column, values) {
|
|
65
|
+
this._filters.push({ col: column, op: 'in', val: values });
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
is(column, value) {
|
|
70
|
+
// value should be null, true, or false
|
|
71
|
+
this._filters.push({ col: column, op: 'is', val: value });
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Modifiers ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
order(column, { ascending = true } = {}) {
|
|
78
|
+
this._orderCol = column;
|
|
79
|
+
this._orderAsc = ascending;
|
|
26
80
|
return this;
|
|
27
81
|
}
|
|
28
82
|
|
|
@@ -31,12 +85,21 @@ class QueryBuilder {
|
|
|
31
85
|
return this;
|
|
32
86
|
}
|
|
33
87
|
|
|
88
|
+
range(from, to) {
|
|
89
|
+
// e.g. range(0, 9) => LIMIT 10 OFFSET 0
|
|
90
|
+
this._limitVal = to - from + 1;
|
|
91
|
+
this._offsetVal = from;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
34
95
|
single() {
|
|
35
96
|
this._single = true;
|
|
36
97
|
this._limitVal = 1;
|
|
37
98
|
return this;
|
|
38
99
|
}
|
|
39
100
|
|
|
101
|
+
// ─── Mutation operators ────────────────────────────────────────────────────
|
|
102
|
+
|
|
40
103
|
insert(data) {
|
|
41
104
|
this._operation = 'insert';
|
|
42
105
|
this._insertData = data;
|
|
@@ -54,16 +117,47 @@ class QueryBuilder {
|
|
|
54
117
|
return this;
|
|
55
118
|
}
|
|
56
119
|
|
|
120
|
+
// ─── SQL Builder ───────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
_filterToSQL(f) {
|
|
123
|
+
const col = `"${f.col}"`;
|
|
124
|
+
switch (f.op) {
|
|
125
|
+
case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
|
|
126
|
+
case 'gt': return `${col} > ${f.val}`;
|
|
127
|
+
case 'lt': return `${col} < ${f.val}`;
|
|
128
|
+
case 'gte': return `${col} >= ${f.val}`;
|
|
129
|
+
case 'lte': return `${col} <= ${f.val}`;
|
|
130
|
+
case 'like': return `${col} LIKE '${f.val}'`;
|
|
131
|
+
case 'ilike': return `${col} ILIKE '${f.val}'`;
|
|
132
|
+
case 'in': {
|
|
133
|
+
const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
|
|
134
|
+
return `${col} IN (${list})`;
|
|
135
|
+
}
|
|
136
|
+
case 'is': {
|
|
137
|
+
if (f.val === null) return `${col} IS NULL`;
|
|
138
|
+
if (f.val === true) return `${col} IS TRUE`;
|
|
139
|
+
if (f.val === false) return `${col} IS FALSE`;
|
|
140
|
+
return `${col} IS NULL`;
|
|
141
|
+
}
|
|
142
|
+
default: return `${col} = '${f.val}'`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
57
146
|
_buildSQL() {
|
|
58
147
|
let sql = '';
|
|
59
148
|
|
|
60
149
|
if (this._operation === 'select') {
|
|
61
150
|
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
151
|
+
|
|
152
|
+
if (this._filters.length) {
|
|
153
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
154
|
+
sql += ` WHERE ${where}`;
|
|
155
|
+
}
|
|
156
|
+
if (this._orderCol) {
|
|
157
|
+
sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
|
|
158
|
+
}
|
|
66
159
|
if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
|
|
160
|
+
if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
|
|
67
161
|
|
|
68
162
|
} else if (this._operation === 'insert') {
|
|
69
163
|
const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
|
|
@@ -76,25 +170,27 @@ class QueryBuilder {
|
|
|
76
170
|
const setClauses = Object.entries(this._updateData).map(([col, val]) =>
|
|
77
171
|
typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
|
|
78
172
|
).join(', ');
|
|
79
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
80
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
81
|
-
);
|
|
82
173
|
sql = `UPDATE "${this._table}" SET ${setClauses}`;
|
|
83
|
-
if (
|
|
174
|
+
if (this._filters.length) {
|
|
175
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
176
|
+
sql += ` WHERE ${where}`;
|
|
177
|
+
}
|
|
84
178
|
sql += ` RETURNING *`;
|
|
85
179
|
|
|
86
180
|
} else if (this._operation === 'delete') {
|
|
87
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
88
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
89
|
-
);
|
|
90
181
|
sql = `DELETE FROM "${this._table}"`;
|
|
91
|
-
if (
|
|
182
|
+
if (this._filters.length) {
|
|
183
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
184
|
+
sql += ` WHERE ${where}`;
|
|
185
|
+
}
|
|
92
186
|
sql += ` RETURNING *`;
|
|
93
187
|
}
|
|
94
188
|
|
|
95
189
|
return sql;
|
|
96
190
|
}
|
|
97
191
|
|
|
192
|
+
// ─── Executor ──────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
98
194
|
async _buildAndRun() {
|
|
99
195
|
const sql = this._buildSQL();
|
|
100
196
|
try {
|
|
@@ -139,6 +235,9 @@ class OpenbaseClient {
|
|
|
139
235
|
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
140
236
|
|
|
141
237
|
function createClient(baseUrl, apiKey, dbName) {
|
|
238
|
+
if (!baseUrl) throw new Error('[openbase] Missing baseUrl. Pass your backend URL as the first argument.');
|
|
239
|
+
if (!apiKey) throw new Error('[openbase] Missing apiKey. Pass your anon or service key as the second argument.');
|
|
240
|
+
if (!dbName) throw new Error('[openbase] Missing dbName. Pass your database name as the third argument.');
|
|
142
241
|
return new OpenbaseClient(baseUrl, apiKey, dbName);
|
|
143
242
|
}
|
|
144
243
|
|