@forestadmin-experimental/datasource-cosmos 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.
- package/README.md +512 -0
- package/dist/collection.d.ts +14 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +90 -0
- package/dist/datasource.d.ts +26 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/datasource.js +87 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/introspection/builder.d.ts +68 -0
- package/dist/introspection/builder.d.ts.map +1 -0
- package/dist/introspection/builder.js +31 -0
- package/dist/introspection/container-introspector.d.ts +27 -0
- package/dist/introspection/container-introspector.d.ts.map +1 -0
- package/dist/introspection/container-introspector.js +168 -0
- package/dist/introspection/introspector.d.ts +18 -0
- package/dist/introspection/introspector.d.ts.map +1 -0
- package/dist/introspection/introspector.js +55 -0
- package/dist/model-builder/model.d.ts +79 -0
- package/dist/model-builder/model.d.ts.map +1 -0
- package/dist/model-builder/model.js +176 -0
- package/dist/utils/aggregation-converter.d.ts +30 -0
- package/dist/utils/aggregation-converter.d.ts.map +1 -0
- package/dist/utils/aggregation-converter.js +181 -0
- package/dist/utils/model-to-collection-schema-converter.d.ts +9 -0
- package/dist/utils/model-to-collection-schema-converter.d.ts.map +1 -0
- package/dist/utils/model-to-collection-schema-converter.js +72 -0
- package/dist/utils/query-converter.d.ts +38 -0
- package/dist/utils/query-converter.d.ts.map +1 -0
- package/dist/utils/query-converter.js +199 -0
- package/dist/utils/serializer.d.ts +6 -0
- package/dist/utils/serializer.d.ts.map +1 -0
- package/dist/utils/serializer.js +29 -0
- package/dist/utils/type-converter.d.ts +43 -0
- package/dist/utils/type-converter.d.ts.map +1 -0
- package/dist/utils/type-converter.js +157 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Forest Admin Cosmos DB NoSQL Datasource
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@forestadmin-experimental/datasource-cosmos)
|
|
4
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
5
|
+
|
|
6
|
+
A Forest Admin datasource for Azure Cosmos DB NoSQL API with full support for:
|
|
7
|
+
- 🔍 **Introspection** - Automatic schema detection from sample documents
|
|
8
|
+
- 📝 **CRUD Operations** - Create, Read, Update, Delete
|
|
9
|
+
- 📊 **Aggregations** - Sum, Count, Avg, Min, Max with grouping
|
|
10
|
+
- 🔎 **Advanced Filtering** - Complex queries with AND/OR conditions
|
|
11
|
+
- 🔢 **Sorting & Pagination** - Efficient data retrieval
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @forestadmin-experimental/datasource-cosmos @azure/cosmos
|
|
17
|
+
# or
|
|
18
|
+
yarn add @forestadmin-experimental/datasource-cosmos @azure/cosmos
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Using the Cosmos DB Emulator (for development)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createAgent } from '@forestadmin/agent';
|
|
27
|
+
import { createCosmosDataSourceForEmulator } from '@forestadmin-experimental/datasource-cosmos';
|
|
28
|
+
|
|
29
|
+
const agent = createAgent({
|
|
30
|
+
authSecret: process.env.FOREST_AUTH_SECRET,
|
|
31
|
+
envSecret: process.env.FOREST_ENV_SECRET,
|
|
32
|
+
isProduction: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
agent.addDataSource(
|
|
36
|
+
createCosmosDataSourceForEmulator('myDatabase', {
|
|
37
|
+
builder: configurator =>
|
|
38
|
+
configurator
|
|
39
|
+
.addCollectionFromContainer({
|
|
40
|
+
name: 'Users',
|
|
41
|
+
databaseName: 'myDatabase',
|
|
42
|
+
containerName: 'users',
|
|
43
|
+
})
|
|
44
|
+
.addCollectionFromContainer({
|
|
45
|
+
name: 'Products',
|
|
46
|
+
databaseName: 'myDatabase',
|
|
47
|
+
containerName: 'products',
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
agent.start();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Using Azure Cosmos DB
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createAgent } from '@forestadmin/agent';
|
|
59
|
+
import { createCosmosDataSource } from '@forestadmin-experimental/datasource-cosmos';
|
|
60
|
+
|
|
61
|
+
const agent = createAgent({
|
|
62
|
+
authSecret: process.env.FOREST_AUTH_SECRET,
|
|
63
|
+
envSecret: process.env.FOREST_ENV_SECRET,
|
|
64
|
+
isProduction: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
agent.addDataSource(
|
|
68
|
+
createCosmosDataSource(
|
|
69
|
+
process.env.COSMOS_ENDPOINT,
|
|
70
|
+
process.env.COSMOS_KEY,
|
|
71
|
+
'myDatabase',
|
|
72
|
+
{
|
|
73
|
+
builder: configurator =>
|
|
74
|
+
configurator
|
|
75
|
+
.addCollectionFromContainer({
|
|
76
|
+
name: 'Users',
|
|
77
|
+
databaseName: 'myDatabase',
|
|
78
|
+
containerName: 'users',
|
|
79
|
+
partitionKeyPath: '/userId',
|
|
80
|
+
})
|
|
81
|
+
.addCollectionFromContainer({
|
|
82
|
+
name: 'Orders',
|
|
83
|
+
databaseName: 'myDatabase',
|
|
84
|
+
containerName: 'orders',
|
|
85
|
+
partitionKeyPath: '/customerId',
|
|
86
|
+
sampleSize: 200, // Analyze 200 documents for schema inference
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
agent.start();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration Options
|
|
96
|
+
|
|
97
|
+
### Factory Functions
|
|
98
|
+
|
|
99
|
+
#### `createCosmosDataSource(endpoint, key, databaseName?, options?)`
|
|
100
|
+
|
|
101
|
+
Create a datasource with Azure Cosmos DB connection details.
|
|
102
|
+
|
|
103
|
+
**Parameters:**
|
|
104
|
+
- `endpoint` (string): Cosmos DB endpoint URL
|
|
105
|
+
- `key` (string): Cosmos DB access key
|
|
106
|
+
- `databaseName` (string, optional): Database name for auto-introspection
|
|
107
|
+
- `options` (object, optional):
|
|
108
|
+
- `builder`: Configuration function for manual collection setup
|
|
109
|
+
- `liveQueryConnections`: Name for native query connection
|
|
110
|
+
- `liveQueryDatabase`: Database for native queries
|
|
111
|
+
- `clientOptions`: Additional CosmosClient options
|
|
112
|
+
|
|
113
|
+
**Example:**
|
|
114
|
+
```typescript
|
|
115
|
+
createCosmosDataSource(
|
|
116
|
+
'https://myaccount.documents.azure.com:443/',
|
|
117
|
+
'myAccessKey',
|
|
118
|
+
'myDatabase',
|
|
119
|
+
{
|
|
120
|
+
builder: configurator =>
|
|
121
|
+
configurator.addCollectionFromContainer({
|
|
122
|
+
name: 'Users',
|
|
123
|
+
databaseName: 'myDatabase',
|
|
124
|
+
containerName: 'users',
|
|
125
|
+
}),
|
|
126
|
+
liveQueryConnections: 'cosmos',
|
|
127
|
+
liveQueryDatabase: 'myDatabase',
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `createCosmosDataSourceForEmulator(databaseName?, options?)`
|
|
133
|
+
|
|
134
|
+
Create a datasource for the Cosmos DB Emulator (localhost:8081).
|
|
135
|
+
|
|
136
|
+
**Parameters:**
|
|
137
|
+
- `databaseName` (string, optional): Database name for auto-introspection
|
|
138
|
+
- `options` (object, optional): Same as `createCosmosDataSource`
|
|
139
|
+
|
|
140
|
+
**Example:**
|
|
141
|
+
```typescript
|
|
142
|
+
createCosmosDataSourceForEmulator('testDatabase', {
|
|
143
|
+
builder: configurator =>
|
|
144
|
+
configurator.addCollectionFromContainer({
|
|
145
|
+
name: 'TestCollection',
|
|
146
|
+
databaseName: 'testDatabase',
|
|
147
|
+
containerName: 'test-container',
|
|
148
|
+
}),
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `createCosmosDataSourceWithExistingClient(client, databaseName?)`
|
|
153
|
+
|
|
154
|
+
Create a datasource with an existing CosmosClient instance.
|
|
155
|
+
|
|
156
|
+
**Parameters:**
|
|
157
|
+
- `client` (CosmosClient): Existing Cosmos DB client
|
|
158
|
+
- `databaseName` (string, optional): Database name for auto-introspection
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
```typescript
|
|
162
|
+
import { CosmosClient } from '@azure/cosmos';
|
|
163
|
+
import { createCosmosDataSourceWithExistingClient } from '@forestadmin-experimental/datasource-cosmos';
|
|
164
|
+
|
|
165
|
+
const client = new CosmosClient({
|
|
166
|
+
endpoint: 'https://myaccount.documents.azure.com:443/',
|
|
167
|
+
key: 'myAccessKey',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
createCosmosDataSourceWithExistingClient(client, 'myDatabase')
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Collection Configuration
|
|
174
|
+
|
|
175
|
+
When using the builder pattern, you can configure collections with these options:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
configurator.addCollectionFromContainer({
|
|
179
|
+
// Required
|
|
180
|
+
name: 'Users', // Forest Admin collection name
|
|
181
|
+
databaseName: 'myDatabase', // Cosmos DB database name
|
|
182
|
+
containerName: 'users', // Cosmos DB container name
|
|
183
|
+
|
|
184
|
+
// Optional
|
|
185
|
+
partitionKeyPath: '/userId', // Partition key (auto-detected if not provided)
|
|
186
|
+
sampleSize: 100, // Number of documents to analyze for schema (default: 100)
|
|
187
|
+
enableCount: true, // Enable total count in pagination (default: true)
|
|
188
|
+
|
|
189
|
+
overrideTypeConverter: (field) => {
|
|
190
|
+
// Custom type conversion logic
|
|
191
|
+
if (field.fieldName === 'customField') {
|
|
192
|
+
return {
|
|
193
|
+
...field.generatedFieldSchema,
|
|
194
|
+
columnType: 'String',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Features
|
|
202
|
+
|
|
203
|
+
### Schema Introspection
|
|
204
|
+
|
|
205
|
+
The datasource automatically analyzes sample documents to infer the schema:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Automatic introspection of all containers in a database
|
|
209
|
+
createCosmosDataSource(endpoint, key, 'myDatabase')
|
|
210
|
+
|
|
211
|
+
// Manual configuration for specific containers
|
|
212
|
+
createCosmosDataSource(endpoint, key, undefined, {
|
|
213
|
+
builder: configurator =>
|
|
214
|
+
configurator
|
|
215
|
+
.addCollectionFromContainer({
|
|
216
|
+
name: 'Users',
|
|
217
|
+
databaseName: 'myDatabase',
|
|
218
|
+
containerName: 'users',
|
|
219
|
+
sampleSize: 200, // Analyze 200 documents for better accuracy
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Supported Data Types:**
|
|
225
|
+
- `string` → Forest Admin `String`
|
|
226
|
+
- `number` → Forest Admin `Number`
|
|
227
|
+
- `boolean` → Forest Admin `Boolean`
|
|
228
|
+
- `date` (ISO 8601 strings or Date objects) → Forest Admin `Date`
|
|
229
|
+
- `object` → Forest Admin `Json`
|
|
230
|
+
- `array` → Forest Admin `Json`
|
|
231
|
+
- GeoJSON Point → Forest Admin `Point`
|
|
232
|
+
|
|
233
|
+
### CRUD Operations
|
|
234
|
+
|
|
235
|
+
All standard CRUD operations are fully supported:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Create
|
|
239
|
+
await collection.create(caller, [
|
|
240
|
+
{ name: 'John Doe', email: 'john@example.com', age: 30 },
|
|
241
|
+
{ name: 'Jane Doe', email: 'jane@example.com', age: 28 },
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
// Read with filters
|
|
245
|
+
await collection.list(
|
|
246
|
+
caller,
|
|
247
|
+
new PaginatedFilter({
|
|
248
|
+
conditionTree: {
|
|
249
|
+
field: 'age',
|
|
250
|
+
operator: 'GreaterThan',
|
|
251
|
+
value: 25,
|
|
252
|
+
},
|
|
253
|
+
page: { limit: 10, skip: 0 },
|
|
254
|
+
}),
|
|
255
|
+
new Projection('id', 'name', 'email'),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Update
|
|
259
|
+
await collection.update(
|
|
260
|
+
caller,
|
|
261
|
+
new Filter({
|
|
262
|
+
conditionTree: {
|
|
263
|
+
field: 'status',
|
|
264
|
+
operator: 'Equal',
|
|
265
|
+
value: 'pending',
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
{ status: 'active' },
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Delete
|
|
272
|
+
await collection.delete(
|
|
273
|
+
caller,
|
|
274
|
+
new Filter({
|
|
275
|
+
conditionTree: {
|
|
276
|
+
field: 'status',
|
|
277
|
+
operator: 'Equal',
|
|
278
|
+
value: 'archived',
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Filtering
|
|
285
|
+
|
|
286
|
+
**Supported Operators:**
|
|
287
|
+
- Presence: `Present`, `Missing`
|
|
288
|
+
- Equality: `Equal`, `NotEqual`, `In`, `NotIn`
|
|
289
|
+
- Comparison: `LessThan`, `GreaterThan`
|
|
290
|
+
- Strings: `Like`, `ILike`, `Contains`, `NotContains`, `StartsWith`, `EndsWith`
|
|
291
|
+
- Arrays: `IncludesAll`
|
|
292
|
+
|
|
293
|
+
**Complex Queries:**
|
|
294
|
+
```typescript
|
|
295
|
+
// AND condition
|
|
296
|
+
const filter = new PaginatedFilter({
|
|
297
|
+
conditionTree: {
|
|
298
|
+
aggregator: 'And',
|
|
299
|
+
conditions: [
|
|
300
|
+
{ field: 'status', operator: 'Equal', value: 'active' },
|
|
301
|
+
{ field: 'age', operator: 'GreaterThan', value: 18 },
|
|
302
|
+
{ field: 'role', operator: 'In', value: ['admin', 'moderator'] },
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// OR condition
|
|
308
|
+
const filter = new PaginatedFilter({
|
|
309
|
+
conditionTree: {
|
|
310
|
+
aggregator: 'Or',
|
|
311
|
+
conditions: [
|
|
312
|
+
{ field: 'priority', operator: 'Equal', value: 'high' },
|
|
313
|
+
{ field: 'urgent', operator: 'Equal', value: true },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Nested conditions
|
|
319
|
+
const filter = new PaginatedFilter({
|
|
320
|
+
conditionTree: {
|
|
321
|
+
aggregator: 'And',
|
|
322
|
+
conditions: [
|
|
323
|
+
{ field: 'status', operator: 'Equal', value: 'active' },
|
|
324
|
+
{
|
|
325
|
+
aggregator: 'Or',
|
|
326
|
+
conditions: [
|
|
327
|
+
{ field: 'role', operator: 'Equal', value: 'admin' },
|
|
328
|
+
{ field: 'role', operator: 'Equal', value: 'owner' },
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Aggregations
|
|
337
|
+
|
|
338
|
+
Full support for aggregations with grouping:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// Simple count
|
|
342
|
+
await collection.aggregate(
|
|
343
|
+
caller,
|
|
344
|
+
new Filter({}),
|
|
345
|
+
{
|
|
346
|
+
operation: 'Count',
|
|
347
|
+
field: null,
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Sum with grouping
|
|
352
|
+
await collection.aggregate(
|
|
353
|
+
caller,
|
|
354
|
+
new Filter({}),
|
|
355
|
+
{
|
|
356
|
+
operation: 'Sum',
|
|
357
|
+
field: 'revenue',
|
|
358
|
+
groups: [{ field: 'category' }],
|
|
359
|
+
},
|
|
360
|
+
10, // limit to 10 groups
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Average by date
|
|
364
|
+
await collection.aggregate(
|
|
365
|
+
caller,
|
|
366
|
+
new Filter({}),
|
|
367
|
+
{
|
|
368
|
+
operation: 'Avg',
|
|
369
|
+
field: 'score',
|
|
370
|
+
groups: [{ field: 'createdAt', operation: 'Month' }],
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Supported Aggregation Operations:**
|
|
376
|
+
- `Count` - Count records
|
|
377
|
+
- `Sum` - Sum numeric field
|
|
378
|
+
- `Avg` - Average of numeric field
|
|
379
|
+
- `Min` - Minimum value
|
|
380
|
+
- `Max` - Maximum value
|
|
381
|
+
|
|
382
|
+
**Supported Date Grouping:**
|
|
383
|
+
- `Year`
|
|
384
|
+
- `Month`
|
|
385
|
+
- `Week` (approximation)
|
|
386
|
+
- `Day`
|
|
387
|
+
|
|
388
|
+
### Sorting & Pagination
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Sort by single field
|
|
392
|
+
const filter = new PaginatedFilter({
|
|
393
|
+
sort: [{ field: 'createdAt', ascending: false }],
|
|
394
|
+
page: { limit: 20, skip: 0 },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Sort by multiple fields
|
|
398
|
+
const filter = new PaginatedFilter({
|
|
399
|
+
sort: [
|
|
400
|
+
{ field: 'status', ascending: true },
|
|
401
|
+
{ field: 'priority', ascending: false },
|
|
402
|
+
{ field: 'createdAt', ascending: false },
|
|
403
|
+
],
|
|
404
|
+
page: { limit: 50, skip: 100 },
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Native SQL Queries
|
|
409
|
+
|
|
410
|
+
Execute native Cosmos DB SQL queries:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
createCosmosDataSource(endpoint, key, 'myDatabase', {
|
|
414
|
+
liveQueryConnections: 'cosmos',
|
|
415
|
+
liveQueryDatabase: 'myDatabase',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// In your Forest Admin dashboard, you can now execute:
|
|
419
|
+
// SELECT * FROM users WHERE users.age > $minAge AND users.status = $status
|
|
420
|
+
// With parameters: { minAge: 18, status: 'active' }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Development
|
|
424
|
+
|
|
425
|
+
### Running Tests
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
# Unit tests
|
|
429
|
+
yarn test
|
|
430
|
+
|
|
431
|
+
# Integration tests (requires Cosmos DB Emulator)
|
|
432
|
+
docker compose up -d
|
|
433
|
+
yarn test
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Cosmos DB Emulator
|
|
437
|
+
|
|
438
|
+
For local development, use the Azure Cosmos DB Emulator:
|
|
439
|
+
|
|
440
|
+
**Docker:**
|
|
441
|
+
```bash
|
|
442
|
+
docker compose up -d
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Connection Details:**
|
|
446
|
+
- Endpoint: `https://localhost:8081`
|
|
447
|
+
- Key: `C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==`
|
|
448
|
+
|
|
449
|
+
## Architecture
|
|
450
|
+
|
|
451
|
+
The datasource follows the Forest Admin datasource architecture:
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
src/
|
|
455
|
+
├── datasource.ts # Main CosmosDataSource class
|
|
456
|
+
├── collection.ts # CosmosCollection with CRUD operations
|
|
457
|
+
├── model-builder/
|
|
458
|
+
│ └── model.ts # ModelCosmos - Cosmos DB client wrapper
|
|
459
|
+
├── introspection/
|
|
460
|
+
│ ├── introspector.ts # Auto-discovery of containers
|
|
461
|
+
│ ├── container-introspector.ts # Schema inference from documents
|
|
462
|
+
│ └── builder.ts # Configuration builder pattern
|
|
463
|
+
├── utils/
|
|
464
|
+
│ ├── type-converter.ts # Cosmos to Forest Admin type mapping
|
|
465
|
+
│ ├── query-converter.ts # Filter to Cosmos SQL conversion
|
|
466
|
+
│ ├── aggregation-converter.ts # Aggregation query builder
|
|
467
|
+
│ ├── model-to-collection-schema-converter.ts # Schema generation
|
|
468
|
+
│ └── serializer.ts # Result serialization
|
|
469
|
+
└── index.ts # Public API and factory functions
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Limitations
|
|
473
|
+
|
|
474
|
+
1. **Cosmos DB SQL API Limitations:**
|
|
475
|
+
- GROUP BY with multiple fields requires complex implementation
|
|
476
|
+
- No native JOIN support (use nested objects instead)
|
|
477
|
+
- Partition key required for efficient queries
|
|
478
|
+
|
|
479
|
+
2. **Performance Considerations:**
|
|
480
|
+
- Schema introspection analyzes sample documents (configurable sample size)
|
|
481
|
+
- Large result sets should use pagination
|
|
482
|
+
- Consider indexing policies for optimal query performance
|
|
483
|
+
|
|
484
|
+
3. **Type Inference:**
|
|
485
|
+
- Schema is inferred from sample documents
|
|
486
|
+
- Mixed types in the same field are treated as Json
|
|
487
|
+
- Nested objects are flattened or treated as Json
|
|
488
|
+
|
|
489
|
+
## Contributing
|
|
490
|
+
|
|
491
|
+
Contributions are welcome! Please see the main repository for contribution guidelines.
|
|
492
|
+
|
|
493
|
+
## License
|
|
494
|
+
|
|
495
|
+
GPL-3.0 - See LICENSE file for details
|
|
496
|
+
|
|
497
|
+
## Support
|
|
498
|
+
|
|
499
|
+
- 📚 [Forest Admin Documentation](https://docs.forestadmin.com)
|
|
500
|
+
- 💬 [Community Forum](https://community.forestadmin.com)
|
|
501
|
+
- 🐛 [Issue Tracker](https://github.com/ForestAdmin/forestadmin-experimental/issues)
|
|
502
|
+
- 📧 [Email Support](mailto:support@forestadmin.com)
|
|
503
|
+
|
|
504
|
+
## Related Packages
|
|
505
|
+
|
|
506
|
+
- [@forestadmin/agent](https://www.npmjs.com/package/@forestadmin/agent) - Forest Admin Agent
|
|
507
|
+
- [@forestadmin/datasource-toolkit](https://www.npmjs.com/package/@forestadmin/datasource-toolkit) - Base toolkit
|
|
508
|
+
- [@azure/cosmos](https://www.npmjs.com/package/@azure/cosmos) - Azure Cosmos DB SDK
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
Made with ❤️ by [Forest Admin](https://www.forestadmin.com)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CosmosClient } from '@azure/cosmos';
|
|
2
|
+
import { AggregateResult, Aggregation, BaseCollection, Caller, DataSource, Filter, Logger, PaginatedFilter, Projection, RecordData } from '@forestadmin/datasource-toolkit';
|
|
3
|
+
import ModelCosmos from './model-builder/model';
|
|
4
|
+
export default class CosmosCollection extends BaseCollection {
|
|
5
|
+
protected internalModel: ModelCosmos;
|
|
6
|
+
private queryConverter;
|
|
7
|
+
constructor(datasource: DataSource, model: ModelCosmos, logger: Logger, nativeDriver: CosmosClient);
|
|
8
|
+
create(caller: Caller, data: RecordData[]): Promise<RecordData[]>;
|
|
9
|
+
list(caller: Caller, filter: PaginatedFilter, projection: Projection): Promise<RecordData[]>;
|
|
10
|
+
update(caller: Caller, filter: Filter, patch: RecordData): Promise<void>;
|
|
11
|
+
delete(caller: Caller, filter: Filter): Promise<void>;
|
|
12
|
+
aggregate(caller: Caller, filter: Filter, aggregation: Aggregation, limit?: number): Promise<AggregateResult[]>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EACL,eAAe,EACf,WAAW,EACX,cAAc,EACd,MAAM,EACN,UAAU,EACV,MAAM,EACN,MAAM,EACN,eAAe,EACf,UAAU,EACV,UAAU,EACX,MAAM,iCAAiC,CAAC;AAEzC,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAKhD,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,cAAc;IAC1D,SAAS,CAAC,aAAa,EAAE,WAAW,CAAC;IAErC,OAAO,CAAC,cAAc,CAAiB;gBAGrC,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,YAAY;IAmBtB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAUjE,IAAI,CACR,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,UAAU,EAAE,CAAC;IAyBlB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrD,SAAS,CACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,WAAW,EACxB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;CAkB9B"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const datasource_toolkit_1 = require("@forestadmin/datasource-toolkit");
|
|
7
|
+
const aggregation_converter_1 = __importDefault(require("./utils/aggregation-converter"));
|
|
8
|
+
const model_to_collection_schema_converter_1 = __importDefault(require("./utils/model-to-collection-schema-converter"));
|
|
9
|
+
const query_converter_1 = __importDefault(require("./utils/query-converter"));
|
|
10
|
+
class CosmosCollection extends datasource_toolkit_1.BaseCollection {
|
|
11
|
+
constructor(datasource, model, logger, nativeDriver) {
|
|
12
|
+
if (!model)
|
|
13
|
+
throw new Error('Invalid (null) model instance.');
|
|
14
|
+
super(model.name, datasource, nativeDriver);
|
|
15
|
+
this.internalModel = model;
|
|
16
|
+
this.queryConverter = new query_converter_1.default();
|
|
17
|
+
const modelSchema = model_to_collection_schema_converter_1.default.convert(this.internalModel, logger);
|
|
18
|
+
if (this.internalModel.enableCount !== false)
|
|
19
|
+
this.enableCount();
|
|
20
|
+
this.addFields(modelSchema.fields);
|
|
21
|
+
this.addSegments(modelSchema.segments);
|
|
22
|
+
logger('Debug', `CosmosCollection - ${this.name} added`);
|
|
23
|
+
}
|
|
24
|
+
async create(caller, data) {
|
|
25
|
+
try {
|
|
26
|
+
const recordsResponse = await this.internalModel.create(data);
|
|
27
|
+
return recordsResponse;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Failed to create records: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async list(caller, filter, projection) {
|
|
34
|
+
try {
|
|
35
|
+
// Build the SQL query from the filter
|
|
36
|
+
const projectionFields = projection.columns.length > 0 ? projection.columns : undefined;
|
|
37
|
+
const querySpec = this.queryConverter.getSqlQuerySpec(filter.conditionTree, filter.sort, projectionFields);
|
|
38
|
+
// Execute the query with pagination
|
|
39
|
+
const recordsResponse = await this.internalModel.query(querySpec, filter.page?.skip, filter.page?.limit);
|
|
40
|
+
// Apply projection to only return projected fields
|
|
41
|
+
return projection.apply(recordsResponse);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`Failed to list records: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async update(caller, filter, patch) {
|
|
48
|
+
try {
|
|
49
|
+
// First, get the IDs of records to update
|
|
50
|
+
const records = await this.list(caller, filter, new datasource_toolkit_1.Projection('id'));
|
|
51
|
+
const ids = records.map(record => record.id);
|
|
52
|
+
if (ids.length === 0) {
|
|
53
|
+
return; // Nothing to update
|
|
54
|
+
}
|
|
55
|
+
await this.internalModel.update(ids, patch);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error(`Failed to update records: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async delete(caller, filter) {
|
|
62
|
+
try {
|
|
63
|
+
// First, get the IDs of records to delete
|
|
64
|
+
const records = await this.list(caller, filter, new datasource_toolkit_1.Projection('id'));
|
|
65
|
+
const ids = records.map(record => record.id);
|
|
66
|
+
if (ids.length === 0) {
|
|
67
|
+
return; // Nothing to delete
|
|
68
|
+
}
|
|
69
|
+
await this.internalModel.delete(ids);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new Error(`Failed to delete records: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async aggregate(caller, filter, aggregation, limit) {
|
|
76
|
+
try {
|
|
77
|
+
// Build aggregation query
|
|
78
|
+
const querySpec = aggregation_converter_1.default.buildAggregationQuery(aggregation, filter.conditionTree, limit);
|
|
79
|
+
// Execute aggregation query
|
|
80
|
+
const rawResults = await this.internalModel.aggregateQuery(querySpec);
|
|
81
|
+
// Process results into Forest Admin format
|
|
82
|
+
return aggregation_converter_1.default.processAggregationResults(rawResults, aggregation);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error(`Failed to aggregate records: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.default = CosmosCollection;
|
|
90
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29sbGVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9jb2xsZWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBR0Esd0VBV3lDO0FBR3pDLDBGQUFpRTtBQUNqRSx3SEFBMEU7QUFDMUUsOEVBQXFEO0FBRXJELE1BQXFCLGdCQUFpQixTQUFRLG1DQUFjO0lBSzFELFlBQ0UsVUFBc0IsRUFDdEIsS0FBa0IsRUFDbEIsTUFBYyxFQUNkLFlBQTBCO1FBRTFCLElBQUksQ0FBQyxLQUFLO1lBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxnQ0FBZ0MsQ0FBQyxDQUFDO1FBRTlELEtBQUssQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLFVBQVUsRUFBRSxZQUFZLENBQUMsQ0FBQztRQUU1QyxJQUFJLENBQUMsYUFBYSxHQUFHLEtBQUssQ0FBQztRQUUzQixJQUFJLENBQUMsY0FBYyxHQUFHLElBQUkseUJBQWMsRUFBRSxDQUFDO1FBRTNDLE1BQU0sV0FBVyxHQUFHLDhDQUFjLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFFdkUsSUFBSSxJQUFJLENBQUMsYUFBYSxDQUFDLFdBQVcsS0FBSyxLQUFLO1lBQUUsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ2pFLElBQUksQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQ25DLElBQUksQ0FBQyxXQUFXLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBRXZDLE1BQU0sQ0FBQyxPQUFPLEVBQUUsc0JBQXNCLElBQUksQ0FBQyxJQUFJLFFBQVEsQ0FBQyxDQUFDO0lBQzNELENBQUM7SUFFRCxLQUFLLENBQUMsTUFBTSxDQUFDLE1BQWMsRUFBRSxJQUFrQjtRQUM3QyxJQUFJLENBQUM7WUFDSCxNQUFNLGVBQWUsR0FBRyxNQUFNLElBQUksQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBRTlELE9BQU8sZUFBZSxDQUFDO1FBQ3pCLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsTUFBTSxJQUFJLEtBQUssQ0FBQyw2QkFBNkIsS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDaEUsQ0FBQztJQUNILENBQUM7SUFFRCxLQUFLLENBQUMsSUFBSSxDQUNSLE1BQWMsRUFDZCxNQUF1QixFQUN2QixVQUFzQjtRQUV0QixJQUFJLENBQUM7WUFDSCxzQ0FBc0M7WUFDdEMsTUFBTSxnQkFBZ0IsR0FBRyxVQUFVLENBQUMsT0FBTyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztZQUV4RixNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsY0FBYyxDQUFDLGVBQWUsQ0FDbkQsTUFBTSxDQUFDLGFBQWEsRUFDcEIsTUFBTSxDQUFDLElBQUksRUFDWCxnQkFBZ0IsQ0FDakIsQ0FBQztZQUVGLG9DQUFvQztZQUNwQyxNQUFNLGVBQWUsR0FBRyxNQUFNLElBQUksQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUNwRCxTQUFTLEVBQ1QsTUFBTSxDQUFDLElBQUksRUFBRSxJQUFJLEVBQ2pCLE1BQU0sQ0FBQyxJQUFJLEVBQUUsS0FBSyxDQUNuQixDQUFDO1lBRUYsbURBQW1EO1lBQ25ELE9BQU8sVUFBVSxDQUFDLEtBQUssQ0FBQyxlQUFlLENBQUMsQ0FBQztRQUMzQyxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sSUFBSSxLQUFLLENBQUMsMkJBQTJCLEtBQUssQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO1FBQzlELENBQUM7SUFDSCxDQUFDO0lBRUQsS0FBSyxDQUFDLE1BQU0sQ0FBQyxNQUFjLEVBQUUsTUFBYyxFQUFFLEtBQWlCO1FBQzVELElBQUksQ0FBQztZQUNILDBDQUEwQztZQUMxQyxNQUFNLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxJQUFJLCtCQUFVLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztZQUN0RSxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLEVBQVksQ0FBQyxDQUFDO1lBRXZELElBQUksR0FBRyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDckIsT0FBTyxDQUFDLG9CQUFvQjtZQUM5QixDQUFDO1lBRUQsTUFBTSxJQUFJLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDOUMsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixNQUFNLElBQUksS0FBSyxDQUFDLDZCQUE2QixLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQztRQUNoRSxDQUFDO0lBQ0gsQ0FBQztJQUVELEtBQUssQ0FBQyxNQUFNLENBQUMsTUFBYyxFQUFFLE1BQWM7UUFDekMsSUFBSSxDQUFDO1lBQ0gsMENBQTBDO1lBQzFDLE1BQU0sT0FBTyxHQUFHLE1BQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsTUFBTSxFQUFFLElBQUksK0JBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1lBQ3RFLE1BQU0sR0FBRyxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsRUFBWSxDQUFDLENBQUM7WUFFdkQsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO2dCQUNyQixPQUFPLENBQUMsb0JBQW9CO1lBQzlCLENBQUM7WUFFRCxNQUFNLElBQUksQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3ZDLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsTUFBTSxJQUFJLEtBQUssQ0FBQyw2QkFBNkIsS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDaEUsQ0FBQztJQUNILENBQUM7SUFFRCxLQUFLLENBQUMsU0FBUyxDQUNiLE1BQWMsRUFDZCxNQUFjLEVBQ2QsV0FBd0IsRUFDeEIsS0FBYztRQUVkLElBQUksQ0FBQztZQUNILDBCQUEwQjtZQUMxQixNQUFNLFNBQVMsR0FBRywrQkFBb0IsQ0FBQyxxQkFBcUIsQ0FDMUQsV0FBVyxFQUNYLE1BQU0sQ0FBQyxhQUFhLEVBQ3BCLEtBQUssQ0FDTixDQUFDO1lBRUYsNEJBQTRCO1lBQzVCLE1BQU0sVUFBVSxHQUFHLE1BQU0sSUFBSSxDQUFDLGFBQWEsQ0FBQyxjQUFjLENBQUMsU0FBUyxDQUFDLENBQUM7WUFFdEUsMkNBQTJDO1lBQzNDLE9BQU8sK0JBQW9CLENBQUMseUJBQXlCLENBQUMsVUFBVSxFQUFFLFdBQVcsQ0FBQyxDQUFDO1FBQ2pGLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsTUFBTSxJQUFJLEtBQUssQ0FBQyxnQ0FBZ0MsS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7UUFDbkUsQ0FBQztJQUNILENBQUM7Q0FDRjtBQTFIRCxtQ0EwSEMifQ==
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CosmosClient } from '@azure/cosmos';
|
|
2
|
+
import { BaseDataSource, Logger } from '@forestadmin/datasource-toolkit';
|
|
3
|
+
import CosmosCollection from './collection';
|
|
4
|
+
import ModelCosmos from './model-builder/model';
|
|
5
|
+
export default class CosmosDataSource extends BaseDataSource<CosmosCollection> {
|
|
6
|
+
/**
|
|
7
|
+
* Cosmos DB client instance
|
|
8
|
+
*/
|
|
9
|
+
protected cosmosClient: CosmosClient;
|
|
10
|
+
constructor(cosmosClient: CosmosClient, collectionModels: ModelCosmos[], logger: Logger, options?: {
|
|
11
|
+
liveQueryConnections?: string;
|
|
12
|
+
liveQueryDatabase?: string;
|
|
13
|
+
});
|
|
14
|
+
protected createCollections(collectionModels: ModelCosmos[], logger: Logger): Promise<void>;
|
|
15
|
+
executeNativeQuery(connectionName: string, query: string, contextVariables?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Parse the query string and extract container name and build SqlQuerySpec
|
|
18
|
+
* Expected format: "SELECT * FROM containerName WHERE ..."
|
|
19
|
+
*/
|
|
20
|
+
private parseQuery;
|
|
21
|
+
/**
|
|
22
|
+
* Replace placeholders ($variableName) with Cosmos DB parameters (@paramN)
|
|
23
|
+
*/
|
|
24
|
+
private replacePlaceholders;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=datasource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datasource.d.ts","sourceRoot":"","sources":["../src/datasource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAgB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,gBAAgB,MAAM,cAAc,CAAC;AAC5C,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAOhD,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,cAAc,CAAC,gBAAgB,CAAC;IAC5E;;OAEG;IACH,SAAS,CAAC,YAAY,EAAE,YAAY,CAAC;gBAGnC,YAAY,EAAE,YAAY,EAC1B,gBAAgB,EAAE,WAAW,EAAE,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE;cAoBzD,iBAAiB,CAAC,gBAAgB,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM;IAUlE,kBAAkB,CAC/B,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,EACb,gBAAgB,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAC7C,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAiBrC;;;OAGG;IACH,OAAO,CAAC,UAAU;IAwBlB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CA0B5B"}
|