@dxos/echo 0.8.2-main.5ca3450 → 0.8.2-main.600d381
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/types/src/query/api.d.ts +90 -23
- package/dist/types/src/query/api.d.ts.map +1 -1
- package/dist/types/src/query/ast.d.ts +95 -137
- package/dist/types/src/query/ast.d.ts.map +1 -1
- package/package.json +13 -13
- package/src/query/api.ts +283 -76
- package/src/query/ast.ts +120 -90
- package/src/query/query.test.ts +37 -26
- package/src/type/Type.test.ts +37 -18
package/src/query/api.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Schema } from 'effect';
|
|
6
|
+
import type { Simplify } from 'effect/Schema';
|
|
6
7
|
|
|
7
8
|
import { raise } from '@dxos/debug';
|
|
8
9
|
import { getSchemaDXN, type Ref } from '@dxos/echo-schema';
|
|
@@ -17,7 +18,15 @@ export interface Query<T> {
|
|
|
17
18
|
// TODO(dmaretskyi): See new effect-schema approach to variance.
|
|
18
19
|
'~Query': { value: T };
|
|
19
20
|
|
|
20
|
-
ast: QueryAST.
|
|
21
|
+
ast: QueryAST.Query;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter the current selection based on a filter.
|
|
25
|
+
* @param filter - Filter to select the objects.
|
|
26
|
+
* @returns Query for the selected objects.
|
|
27
|
+
*/
|
|
28
|
+
select(filter: Filter<T>): Query<T>;
|
|
29
|
+
select(props: Filter.Props<T>): Query<T>;
|
|
21
30
|
|
|
22
31
|
/**
|
|
23
32
|
* Traverse an outgoing reference.
|
|
@@ -46,7 +55,7 @@ export interface Query<T> {
|
|
|
46
55
|
*/
|
|
47
56
|
sourceOf<S extends Schema.Schema.All>(
|
|
48
57
|
relation: S,
|
|
49
|
-
predicates?:
|
|
58
|
+
predicates?: Filter.Props<Schema.Schema.Type<S>>,
|
|
50
59
|
): Query<Schema.Schema.Type<S>>;
|
|
51
60
|
|
|
52
61
|
/**
|
|
@@ -57,7 +66,7 @@ export interface Query<T> {
|
|
|
57
66
|
*/
|
|
58
67
|
targetOf<S extends Schema.Schema.All>(
|
|
59
68
|
relation: S,
|
|
60
|
-
predicates?:
|
|
69
|
+
predicates?: Filter.Props<Schema.Schema.Type<S>>,
|
|
61
70
|
): Query<Schema.Schema.Type<S>>;
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -74,17 +83,80 @@ export interface Query<T> {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
interface QueryAPI {
|
|
86
|
+
/**
|
|
87
|
+
* Select objects based on a filter.
|
|
88
|
+
* @param filter - Filter to select the objects.
|
|
89
|
+
* @returns Query for the selected objects.
|
|
90
|
+
*/
|
|
91
|
+
select<F extends Filter.Any>(filter: F): Query<Filter.Type<F>>;
|
|
92
|
+
|
|
77
93
|
/**
|
|
78
94
|
* Query for objects of a given schema.
|
|
79
95
|
* @param schema - Schema of the objects.
|
|
80
96
|
* @param predicates - Predicates to filter the objects.
|
|
81
97
|
* @returns Query for the objects.
|
|
98
|
+
*
|
|
99
|
+
* Shorthand for: `Query.select(Filter.type(schema, predicates))`.
|
|
82
100
|
*/
|
|
83
101
|
type<S extends Schema.Schema.All>(
|
|
84
102
|
schema: S,
|
|
85
|
-
predicates?:
|
|
103
|
+
predicates?: Filter.Props<Schema.Schema.Type<S>>,
|
|
86
104
|
): Query<Schema.Schema.Type<S>>;
|
|
87
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Full-text or vector search.
|
|
108
|
+
*
|
|
109
|
+
* @deprecated Use `Filter`.
|
|
110
|
+
*/
|
|
111
|
+
// text<S extends Schema.Schema.All>(
|
|
112
|
+
// // TODO(dmaretskyi): Allow passing an array of schema here.
|
|
113
|
+
// schema: S,
|
|
114
|
+
// // TODO(dmaretskyi): Consider passing a vector here, but really the embedding should be done on the query-executor side.
|
|
115
|
+
// text: string,
|
|
116
|
+
// options?: Query.TextSearchOptions,
|
|
117
|
+
// ): Query<Schema.Schema.Type<S>>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Combine results of multiple queries.
|
|
121
|
+
* @param queries - Queries to combine.
|
|
122
|
+
* @returns Query for the combined results.
|
|
123
|
+
*/
|
|
124
|
+
all<T>(...queries: Query<T>[]): Query<T>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export declare namespace Query {
|
|
128
|
+
export type TextSearchOptions = {
|
|
129
|
+
type?: 'full-text' | 'vector';
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface Filter<T> {
|
|
134
|
+
// TODO(dmaretskyi): See new effect-schema approach to variance.
|
|
135
|
+
'~Filter': { value: T };
|
|
136
|
+
|
|
137
|
+
ast: QueryAST.Filter;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type Intersection<Types extends readonly unknown[]> = Types extends [infer First, ...infer Rest]
|
|
141
|
+
? First & Intersection<Rest>
|
|
142
|
+
: unknown;
|
|
143
|
+
|
|
144
|
+
interface FilterAPI {
|
|
145
|
+
is(value: unknown): value is Filter<any>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Filter by type.
|
|
149
|
+
*/
|
|
150
|
+
type<S extends Schema.Schema.All>(
|
|
151
|
+
schema: S,
|
|
152
|
+
props?: Filter.Props<Schema.Schema.Type<S>>,
|
|
153
|
+
): Filter<Schema.Schema.Type<S>>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Filter by properties.
|
|
157
|
+
*/
|
|
158
|
+
// props<T>(props: Filter.Props<T>): Filter<T>;
|
|
159
|
+
|
|
88
160
|
/**
|
|
89
161
|
* Full-text or vector search.
|
|
90
162
|
*/
|
|
@@ -94,101 +166,126 @@ interface QueryAPI {
|
|
|
94
166
|
// TODO(dmaretskyi): Consider passing a vector here, but really the embedding should be done on the query-executor side.
|
|
95
167
|
text: string,
|
|
96
168
|
options?: Query.TextSearchOptions,
|
|
97
|
-
):
|
|
169
|
+
): Filter<Schema.Schema.Type<S>>;
|
|
98
170
|
|
|
99
171
|
/**
|
|
100
|
-
*
|
|
101
|
-
* @param queries - Queries to combine.
|
|
102
|
-
* @returns Query for the combined results.
|
|
172
|
+
* Predicate for property to be equal to the provided value.
|
|
103
173
|
*/
|
|
104
|
-
|
|
174
|
+
eq<T>(value: T): Filter<T>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Predicate for property to be not equal to the provided value.
|
|
178
|
+
*/
|
|
179
|
+
neq<T>(value: T): Filter<T>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Predicate for property to be greater than the provided value.
|
|
183
|
+
*/
|
|
184
|
+
gt<T>(value: T): Filter<T>;
|
|
105
185
|
|
|
106
186
|
/**
|
|
107
187
|
* Predicate for property to be greater than the provided value.
|
|
108
188
|
*/
|
|
109
|
-
gt<T>(value: T):
|
|
189
|
+
gt<T>(value: T): Filter<T>;
|
|
110
190
|
|
|
111
191
|
/**
|
|
112
192
|
* Predicate for property to be greater than or equal to the provided value.
|
|
113
193
|
*/
|
|
114
|
-
gte<T>(value: T):
|
|
194
|
+
gte<T>(value: T): Filter<T>;
|
|
115
195
|
|
|
116
196
|
/**
|
|
117
197
|
* Predicate for property to be less than the provided value.
|
|
118
198
|
*/
|
|
119
|
-
lt<T>(value: T):
|
|
199
|
+
lt<T>(value: T): Filter<T>;
|
|
120
200
|
|
|
121
201
|
/**
|
|
122
202
|
* Predicate for property to be less than or equal to the provided value.
|
|
123
203
|
*/
|
|
124
|
-
lte<T>(value: T):
|
|
204
|
+
lte<T>(value: T): Filter<T>;
|
|
125
205
|
|
|
126
206
|
/**
|
|
127
207
|
* Predicate for property to be in the provided array.
|
|
128
208
|
* @param values - Values to check against.
|
|
129
209
|
*/
|
|
130
|
-
in<T>(...values: T[]):
|
|
210
|
+
in<T>(...values: T[]): Filter<T>;
|
|
131
211
|
|
|
132
212
|
/**
|
|
133
213
|
* Predicate for property to be in the provided range.
|
|
134
214
|
* @param from - Start of the range (inclusive).
|
|
135
215
|
* @param to - End of the range (exclusive).
|
|
136
216
|
*/
|
|
137
|
-
|
|
217
|
+
between<T>(from: T, to: T): Filter<T>;
|
|
138
218
|
|
|
139
|
-
|
|
140
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Negate the filter.
|
|
221
|
+
*/
|
|
222
|
+
not<F extends Filter.Any>(filter: F): Filter<F>;
|
|
141
223
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
224
|
+
/**
|
|
225
|
+
* Combine filters with a logical AND.
|
|
226
|
+
*/
|
|
227
|
+
and<FS extends Filter.Any[]>(...filters: FS): Filter<Filter.And<FS>>;
|
|
147
228
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Combine filters with a logical OR.
|
|
231
|
+
*/
|
|
232
|
+
or<FS extends Filter.Any[]>(...filters: FS): Filter<Filter.Or<FS>>;
|
|
151
233
|
|
|
152
|
-
|
|
234
|
+
// TODO(dmaretskyi): Add `Filter.match` to support pattern matching on string props.
|
|
153
235
|
}
|
|
154
236
|
|
|
155
|
-
|
|
156
|
-
|
|
237
|
+
export declare namespace Filter {
|
|
238
|
+
type Props<T> = {
|
|
239
|
+
// Predicate or a value as a shorthand for `eq`.
|
|
240
|
+
[K in keyof T & string]?: Filter<T[K]> | T[K];
|
|
241
|
+
};
|
|
157
242
|
|
|
158
|
-
|
|
159
|
-
};
|
|
243
|
+
type Any = Filter<any>;
|
|
160
244
|
|
|
161
|
-
type
|
|
162
|
-
// Predicate or a value as a shorthand for `eq`.
|
|
163
|
-
[K in keyof T & string]?: Predicate<T[K]> | T[K];
|
|
164
|
-
};
|
|
245
|
+
type Type<F extends Any> = F extends Filter<infer T> ? T : never;
|
|
165
246
|
|
|
166
|
-
|
|
167
|
-
* All property paths inside T that are references.
|
|
168
|
-
*/
|
|
169
|
-
type RefPropKey<T> = { [K in keyof T]: T[K] extends Ref<infer _U> ? K : never }[keyof T] & string;
|
|
247
|
+
type And<FS extends readonly Any[]> = Simplify<Intersection<{ [K in keyof FS]: Type<FS[K]> }>>;
|
|
170
248
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
Object.entries(predicates).map(([key, predicate]) => [key, predicate.ast]),
|
|
174
|
-
) as QueryAST.PredicateSet;
|
|
175
|
-
};
|
|
249
|
+
type Or<FS extends readonly Any[]> = Simplify<{ [K in keyof FS]: Type<FS[K]> }[number]>;
|
|
250
|
+
}
|
|
176
251
|
|
|
177
|
-
class
|
|
178
|
-
private static variance:
|
|
252
|
+
class FilterClass implements Filter<any> {
|
|
253
|
+
private static variance: Filter<any>['~Filter'] = {} as Filter<any>['~Filter'];
|
|
254
|
+
|
|
255
|
+
static is(value: unknown): value is Filter<any> {
|
|
256
|
+
return typeof value === 'object' && value !== null && '~Filter' in value;
|
|
257
|
+
}
|
|
179
258
|
|
|
180
|
-
static type
|
|
259
|
+
static type<S extends Schema.Schema.All>(
|
|
260
|
+
schema: S,
|
|
261
|
+
props?: Filter.Props<Schema.Schema.Type<S>>,
|
|
262
|
+
): Filter<Schema.Schema.Type<S>> {
|
|
181
263
|
const dxn = getSchemaDXN(schema) ?? raise(new TypeError('Schema has no DXN'));
|
|
182
|
-
return new
|
|
183
|
-
type: '
|
|
264
|
+
return new FilterClass({
|
|
265
|
+
type: 'object',
|
|
184
266
|
typename: dxn.toString(),
|
|
185
|
-
|
|
267
|
+
props: props ? propsFilterToAst(props) : {},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @internal
|
|
273
|
+
*/
|
|
274
|
+
static props<T>(props: Filter.Props<T>): Filter<T> {
|
|
275
|
+
return new FilterClass({
|
|
276
|
+
type: 'object',
|
|
277
|
+
typename: null,
|
|
278
|
+
props: propsFilterToAst(props),
|
|
186
279
|
});
|
|
187
280
|
}
|
|
188
281
|
|
|
189
|
-
static text
|
|
282
|
+
static text<S extends Schema.Schema.All>(
|
|
283
|
+
schema: S,
|
|
284
|
+
text: string,
|
|
285
|
+
options?: Query.TextSearchOptions,
|
|
286
|
+
): Filter<Schema.Schema.Type<S>> {
|
|
190
287
|
const dxn = getSchemaDXN(schema) ?? raise(new TypeError('Schema has no DXN'));
|
|
191
|
-
return new
|
|
288
|
+
return new FilterClass({
|
|
192
289
|
type: 'text-search',
|
|
193
290
|
typename: dxn.toString(),
|
|
194
291
|
text,
|
|
@@ -196,41 +293,155 @@ class QueryClass implements Query<any> {
|
|
|
196
293
|
});
|
|
197
294
|
}
|
|
198
295
|
|
|
199
|
-
static
|
|
200
|
-
return new
|
|
201
|
-
type: '
|
|
202
|
-
|
|
296
|
+
static eq<T>(value: T): Filter<T> {
|
|
297
|
+
return new FilterClass({
|
|
298
|
+
type: 'compare',
|
|
299
|
+
operator: 'eq',
|
|
300
|
+
value,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static neq<T>(value: T): Filter<T> {
|
|
305
|
+
return new FilterClass({
|
|
306
|
+
type: 'compare',
|
|
307
|
+
operator: 'neq',
|
|
308
|
+
value,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
static gt<T>(value: T): Filter<T> {
|
|
313
|
+
return new FilterClass({
|
|
314
|
+
type: 'compare',
|
|
315
|
+
operator: 'gt',
|
|
316
|
+
value,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static gte<T>(value: T): Filter<T> {
|
|
321
|
+
return new FilterClass({
|
|
322
|
+
type: 'compare',
|
|
323
|
+
operator: 'gte',
|
|
324
|
+
value,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static lt<T>(value: T): Filter<T> {
|
|
329
|
+
return new FilterClass({
|
|
330
|
+
type: 'compare',
|
|
331
|
+
operator: 'lt',
|
|
332
|
+
value,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
static lte<T>(value: T): Filter<T> {
|
|
337
|
+
return new FilterClass({
|
|
338
|
+
type: 'compare',
|
|
339
|
+
operator: 'lte',
|
|
340
|
+
value,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static in<T>(...values: T[]): Filter<T> {
|
|
345
|
+
return new FilterClass({
|
|
346
|
+
type: 'in',
|
|
347
|
+
values,
|
|
203
348
|
});
|
|
204
349
|
}
|
|
205
350
|
|
|
206
|
-
static
|
|
207
|
-
return
|
|
351
|
+
static between<T>(from: T, to: T): Filter<T> {
|
|
352
|
+
return new FilterClass({
|
|
353
|
+
type: 'range',
|
|
354
|
+
from,
|
|
355
|
+
to,
|
|
356
|
+
});
|
|
208
357
|
}
|
|
209
358
|
|
|
210
|
-
static
|
|
211
|
-
return
|
|
359
|
+
static not<F extends Filter.Any>(filter: F): Filter<F> {
|
|
360
|
+
return new FilterClass({
|
|
361
|
+
type: 'not',
|
|
362
|
+
filter: filter.ast,
|
|
363
|
+
});
|
|
212
364
|
}
|
|
213
365
|
|
|
214
|
-
static
|
|
215
|
-
return
|
|
366
|
+
static and<T>(...filters: Filter<T>[]): Filter<T> {
|
|
367
|
+
return new FilterClass({
|
|
368
|
+
type: 'and',
|
|
369
|
+
filters: filters.map((f) => f.ast),
|
|
370
|
+
});
|
|
216
371
|
}
|
|
217
372
|
|
|
218
|
-
static
|
|
219
|
-
return
|
|
373
|
+
static or<T>(...filters: Filter<T>[]): Filter<T> {
|
|
374
|
+
return new FilterClass({
|
|
375
|
+
type: 'or',
|
|
376
|
+
filters: filters.map((f) => f.ast),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private constructor(public readonly ast: QueryAST.Filter) {}
|
|
381
|
+
|
|
382
|
+
'~Filter' = FilterClass.variance;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const Filter: FilterAPI = FilterClass;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* All property paths inside T that are references.
|
|
389
|
+
*/
|
|
390
|
+
type RefPropKey<T> = { [K in keyof T]: T[K] extends Ref<infer _U> ? K : never }[keyof T] & string;
|
|
391
|
+
|
|
392
|
+
const propsFilterToAst = (predicates: Filter.Props<any>): Record<string, QueryAST.Filter> => {
|
|
393
|
+
return Object.fromEntries(
|
|
394
|
+
Object.entries(predicates).map(([key, predicate]) => [
|
|
395
|
+
key,
|
|
396
|
+
Filter.is(predicate) ? predicate.ast : Filter.eq(predicate).ast,
|
|
397
|
+
]),
|
|
398
|
+
) as Record<string, QueryAST.Filter>;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
class QueryClass implements Query<any> {
|
|
402
|
+
private static variance: Query<any>['~Query'] = {} as Query<any>['~Query'];
|
|
403
|
+
|
|
404
|
+
static select<F extends Filter.Any>(filter: F): Query<Filter.Type<F>> {
|
|
405
|
+
return new QueryClass({
|
|
406
|
+
type: 'select',
|
|
407
|
+
filter: filter.ast,
|
|
408
|
+
});
|
|
220
409
|
}
|
|
221
410
|
|
|
222
|
-
static
|
|
223
|
-
return
|
|
411
|
+
static type(schema: Schema.Schema.All, predicates?: Filter.Props<unknown>): Query<any> {
|
|
412
|
+
return new QueryClass({
|
|
413
|
+
type: 'select',
|
|
414
|
+
filter: FilterClass.type(schema, predicates).ast,
|
|
415
|
+
});
|
|
224
416
|
}
|
|
225
417
|
|
|
226
|
-
static
|
|
227
|
-
return
|
|
418
|
+
static all(...queries: Query<any>[]): Query<any> {
|
|
419
|
+
return new QueryClass({
|
|
420
|
+
type: 'union',
|
|
421
|
+
queries: queries.map((q) => q.ast),
|
|
422
|
+
});
|
|
228
423
|
}
|
|
229
424
|
|
|
230
|
-
constructor(public readonly ast: QueryAST.
|
|
425
|
+
constructor(public readonly ast: QueryAST.Query) {}
|
|
231
426
|
|
|
232
427
|
'~Query' = QueryClass.variance;
|
|
233
428
|
|
|
429
|
+
select(filter: Filter<any> | Filter.Props<any>): Query<any> {
|
|
430
|
+
if (Filter.is(filter)) {
|
|
431
|
+
return new QueryClass({
|
|
432
|
+
type: 'filter',
|
|
433
|
+
selection: this.ast,
|
|
434
|
+
filter: filter.ast,
|
|
435
|
+
});
|
|
436
|
+
} else {
|
|
437
|
+
return new QueryClass({
|
|
438
|
+
type: 'filter',
|
|
439
|
+
selection: this.ast,
|
|
440
|
+
filter: FilterClass.props(filter).ast,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
234
445
|
reference(key: string): Query<any> {
|
|
235
446
|
return new QueryClass({
|
|
236
447
|
type: 'reference-traversal',
|
|
@@ -249,25 +460,21 @@ class QueryClass implements Query<any> {
|
|
|
249
460
|
});
|
|
250
461
|
}
|
|
251
462
|
|
|
252
|
-
sourceOf(relation: Schema.Schema.All, predicates?:
|
|
253
|
-
const dxn = getSchemaDXN(relation) ?? raise(new TypeError('Relation schema has no DXN'));
|
|
463
|
+
sourceOf(relation: Schema.Schema.All, predicates?: Filter.Props<unknown> | undefined): Query<any> {
|
|
254
464
|
return new QueryClass({
|
|
255
465
|
type: 'relation',
|
|
256
466
|
anchor: this.ast,
|
|
257
467
|
direction: 'outgoing',
|
|
258
|
-
|
|
259
|
-
predicates: predicates ? predicateSetToAst(predicates) : undefined,
|
|
468
|
+
filter: FilterClass.type(relation, predicates).ast,
|
|
260
469
|
});
|
|
261
470
|
}
|
|
262
471
|
|
|
263
|
-
targetOf(relation: Schema.Schema.All, predicates?:
|
|
264
|
-
const dxn = getSchemaDXN(relation) ?? raise(new TypeError('Relation schema has no DXN'));
|
|
472
|
+
targetOf(relation: Schema.Schema.All, predicates?: Filter.Props<unknown> | undefined): Query<any> {
|
|
265
473
|
return new QueryClass({
|
|
266
474
|
type: 'relation',
|
|
267
475
|
anchor: this.ast,
|
|
268
476
|
direction: 'incoming',
|
|
269
|
-
|
|
270
|
-
predicates: predicates ? predicateSetToAst(predicates) : undefined,
|
|
477
|
+
filter: FilterClass.type(relation, predicates).ast,
|
|
271
478
|
});
|
|
272
479
|
}
|
|
273
480
|
|