@travetto/model 8.0.0-alpha.9 → 8.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.
Files changed (42) hide show
  1. package/README.md +91 -102
  2. package/__index__.ts +7 -11
  3. package/package.json +18 -18
  4. package/src/error/exists.ts +1 -1
  5. package/src/error/invalid-index.ts +5 -3
  6. package/src/error/invalid-sub-type.ts +1 -1
  7. package/src/error/not-found.ts +1 -1
  8. package/src/error/unique.ts +13 -0
  9. package/src/registry/decorator.ts +38 -32
  10. package/src/registry/registry-adapter.ts +11 -8
  11. package/src/registry/registry-index.ts +12 -33
  12. package/src/registry/types.ts +24 -39
  13. package/src/types/basic.ts +3 -3
  14. package/src/types/blob.ts +6 -6
  15. package/src/types/bulk.ts +5 -9
  16. package/src/types/crud.ts +9 -6
  17. package/src/types/expiry.ts +2 -2
  18. package/src/types/model.ts +11 -2
  19. package/src/types/storage.ts +2 -3
  20. package/src/util/blob.ts +2 -2
  21. package/src/util/bulk.ts +6 -3
  22. package/src/util/crud.ts +105 -22
  23. package/src/util/expiry.ts +4 -5
  24. package/src/util/storage.ts +3 -4
  25. package/support/base-command.ts +3 -5
  26. package/support/bin/candidate.ts +8 -11
  27. package/support/bin/export.ts +2 -2
  28. package/support/bin/install.ts +2 -2
  29. package/support/cli.model_export.ts +9 -5
  30. package/support/cli.model_install.ts +9 -5
  31. package/support/doc.support.tsx +16 -20
  32. package/support/test/base.ts +14 -12
  33. package/support/test/basic.ts +44 -3
  34. package/support/test/blob.ts +8 -9
  35. package/support/test/bulk.ts +16 -5
  36. package/support/test/crud.ts +134 -82
  37. package/support/test/expiry.ts +39 -21
  38. package/support/test/polymorphism.ts +43 -143
  39. package/support/test/suite.ts +15 -12
  40. package/src/types/indexed.ts +0 -43
  41. package/src/util/indexed.ts +0 -144
  42. package/support/test/indexed.ts +0 -190
@@ -1,17 +1,12 @@
1
1
  import assert from 'node:assert';
2
2
  import timers from 'node:timers/promises';
3
3
 
4
- import { Suite, Test } from '@travetto/test';
4
+ import { Model, type ModelCrudSupport, NotFoundError, PersistValue, SubTypeNotSupportedError } from '@travetto/model';
5
5
  import { castTo } from '@travetto/runtime';
6
- import { Schema, DiscriminatorField, Text, TypeMismatchError, Discriminated } from '@travetto/schema';
7
- import {
8
- type ModelIndexedSupport, Index, type ModelCrudSupport, Model,
9
- NotFoundError, SubTypeNotSupportedError, PersistValue
10
- } from '@travetto/model';
6
+ import { DiscriminatorField, Schema, Text, TypeMismatchError } from '@travetto/schema';
7
+ import { Suite, Test } from '@travetto/test';
11
8
 
12
- import { ModelIndexedUtil } from '../../src/util/indexed.ts';
13
9
  import { ExistsError } from '../../src/error/exists.ts';
14
-
15
10
  import { BaseModelSuite } from './base.ts';
16
11
 
17
12
  @Schema()
@@ -43,45 +38,8 @@ export class Engineer extends Worker {
43
38
  major: string;
44
39
  }
45
40
 
46
- @Model()
47
- @Index({
48
- name: 'worker-name',
49
- type: 'sorted',
50
- fields: [{ name: 1 }, { age: 1 }]
51
- })
52
- @Discriminated('type')
53
- export class IndexedWorker {
54
- id: string;
55
- type: string;
56
- name: string;
57
- age?: number;
58
- }
59
- @Model()
60
- export class IndexedDoctor extends IndexedWorker {
61
- specialty: string;
62
- }
63
-
64
- @Model()
65
- export class IndexedFirefighter extends IndexedWorker {
66
- firehouse: number;
67
- }
68
-
69
- @Model()
70
- export class IndexedEngineer extends IndexedWorker {
71
- major: string;
72
- }
73
-
74
- async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
75
- const out: T[] = [];
76
- for await (const el of iterable) {
77
- out.push(el);
78
- }
79
- return out;
80
- }
81
-
82
41
  @Suite()
83
42
  export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSupport> {
84
-
85
43
  @Test('Polymorphic create and find')
86
44
  async polymorphicCreateAndFind() {
87
45
  const service = await this.service;
@@ -95,9 +53,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
95
53
  assert(doc instanceof Doctor);
96
54
  assert(doc.updatedDate !== undefined);
97
55
 
98
- await assert.rejects(
99
- () => service.get(Engineer, doc.id),
100
- NotFoundError);
56
+ await assert.rejects(() => service.get(Engineer, doc.id), NotFoundError);
101
57
 
102
58
  assert(doc instanceof Doctor);
103
59
  assert(fire instanceof Firefighter);
@@ -108,7 +64,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
108
64
  const fire2 = await service.get(Worker, fire.id);
109
65
  assert(fire2 instanceof Firefighter);
110
66
 
111
- const all = await collect(service.list(Worker));
67
+ const all = await this.toArray(service.list(Worker));
112
68
  assert(all.length === 3);
113
69
 
114
70
  const doc3 = all.find(x => x instanceof Doctor);
@@ -126,18 +82,21 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
126
82
  assert(eng3.major === 'oranges');
127
83
  assert(eng3.name === 'cob');
128
84
 
129
- const engineers = await collect(service.list(Engineer));
85
+ const engineers = await this.toArray(service.list(Engineer));
130
86
  assert(engineers.length === 1);
131
87
 
132
- await service.create(Engineer, Engineer.from({
133
- major: 'foodService',
134
- name: 'bob2'
135
- }));
88
+ await service.create(
89
+ Engineer,
90
+ Engineer.from({
91
+ major: 'foodService',
92
+ name: 'bob2'
93
+ })
94
+ );
136
95
 
137
- const all2 = await collect(service.list(Worker));
96
+ const all2 = await this.toArray(service.list(Worker));
138
97
  assert(all2.length === 4);
139
98
 
140
- const engineers2 = await collect(service.list(Engineer));
99
+ const engineers2 = await this.toArray(service.list(Engineer));
141
100
  assert(engineers2.length === 2);
142
101
  }
143
102
 
@@ -152,16 +111,21 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
152
111
 
153
112
  await this.saveAll(Worker, [doc, fire, eng]);
154
113
 
155
- assert(await service.get(Worker, doc.id) instanceof Doctor);
156
- assert(await service.get(Worker, fire.id) instanceof Firefighter);
114
+ assert((await service.get(Worker, doc.id)) instanceof Doctor);
115
+ assert((await service.get(Worker, fire.id)) instanceof Firefighter);
157
116
 
158
117
  const update = new Date();
159
118
 
160
119
  await assert.rejects(
161
120
  () =>
162
- service.upsert(Doctor, Doctor.from({
163
- id: fire.id, name: 'gob', specialty: 'eyes'
164
- })),
121
+ service.upsert(
122
+ Doctor,
123
+ Doctor.from({
124
+ id: fire.id,
125
+ name: 'gob',
126
+ specialty: 'eyes'
127
+ })
128
+ ),
165
129
  e => e instanceof SubTypeNotSupportedError || e instanceof ExistsError
166
130
  );
167
131
 
@@ -172,18 +136,28 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
172
136
  await timers.setTimeout(15);
173
137
 
174
138
  try {
175
- const result = await service.upsert(Doctor, Doctor.from({
176
- id: doc.id, name: 'gob', specialty: 'eyes'
177
- }));
139
+ const result = await service.upsert(
140
+ Doctor,
141
+ Doctor.from({
142
+ id: doc.id,
143
+ name: 'gob',
144
+ specialty: 'eyes'
145
+ })
146
+ );
178
147
 
179
148
  assert(result.updatedDate!.getTime() > update.getTime());
180
149
  } catch (err) {
181
150
  assert(err instanceof SubTypeNotSupportedError);
182
151
  }
183
152
 
184
- const resAlt = await service.upsert(Worker, Doctor.from({
185
- id: doc.id, name: 'gob', specialty: 'eyes'
186
- }));
153
+ const resAlt = await service.upsert(
154
+ Worker,
155
+ Doctor.from({
156
+ id: doc.id,
157
+ name: 'gob',
158
+ specialty: 'eyes'
159
+ })
160
+ );
187
161
 
188
162
  assert(resAlt.updatedDate!.getTime() > update.getTime());
189
163
 
@@ -196,10 +170,7 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
196
170
  // Delete by base class
197
171
  await service.delete(Worker, fire.id);
198
172
 
199
- await assert.rejects(
200
- () => service.delete(Worker, fire.id),
201
- NotFoundError
202
- );
173
+ await assert.rejects(() => service.delete(Worker, fire.id), NotFoundError);
203
174
 
204
175
  // Delete by any subtype when id is missing
205
176
  await assert.rejects(
@@ -207,75 +178,4 @@ export abstract class ModelPolymorphismSuite extends BaseModelSuite<ModelCrudSup
207
178
  e => e instanceof SubTypeNotSupportedError || e instanceof NotFoundError
208
179
  );
209
180
  }
210
-
211
- @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
212
- async polymorphicIndexGet() {
213
- const service: ModelIndexedSupport = castTo(await this.service);
214
- const now = 30;
215
- const [doc, fire, eng] = [
216
- IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
217
- IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
218
- IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
219
- ];
220
-
221
- await this.saveAll(IndexedWorker, [doc, fire, eng]);
222
-
223
- const result = await service.getByIndex(IndexedWorker, 'worker-name', {
224
- age: now,
225
- name: 'rob'
226
- });
227
-
228
- assert(result instanceof IndexedFirefighter);
229
-
230
- try {
231
- const res2 = await service.getByIndex(IndexedFirefighter, 'worker-name', {
232
- age: now,
233
- name: 'rob'
234
- });
235
- assert(res2 instanceof IndexedFirefighter); // If service allows for get by subtype
236
- } catch (err) {
237
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError); // If it does not
238
- }
239
- }
240
-
241
- @Test('Polymorphic index', { skip: BaseModelSuite.ifNot(ModelIndexedUtil.isSupported) })
242
- async polymorphicIndexDelete() {
243
- const service: ModelIndexedSupport = castTo(await this.service);
244
- const now = 30;
245
- const [doc, fire, eng] = [
246
- IndexedDoctor.from({ name: 'bob', specialty: 'feet', age: now }),
247
- IndexedFirefighter.from({ name: 'rob', firehouse: 20, age: now }),
248
- IndexedEngineer.from({ name: 'cob', major: 'oranges', age: now })
249
- ];
250
-
251
- await this.saveAll(IndexedWorker, [doc, fire, eng]);
252
-
253
- assert(await this.getSize(IndexedWorker) === 3);
254
-
255
- await service.deleteByIndex(IndexedWorker, 'worker-name', {
256
- age: now,
257
- name: 'bob'
258
- });
259
-
260
- assert(await this.getSize(IndexedWorker) === 2);
261
- assert(await this.getSize(IndexedDoctor) === 0);
262
-
263
- try {
264
- await service.deleteByIndex(IndexedFirefighter, 'worker-name', {
265
- age: now,
266
- name: 'rob'
267
- });
268
- } catch (err) {
269
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
270
- }
271
-
272
- try {
273
- await service.deleteByIndex(IndexedEngineer, 'worker-name', {
274
- age: now,
275
- name: 'bob'
276
- });
277
- } catch (err) {
278
- assert(err instanceof SubTypeNotSupportedError || err instanceof NotFoundError);
279
- }
280
- }
281
- }
181
+ }
@@ -1,16 +1,16 @@
1
- import { type Class } from '@travetto/runtime';
2
1
  import { DependencyRegistryIndex } from '@travetto/di';
3
2
  import { Registry } from '@travetto/registry';
4
- import { SuiteRegistryIndex, TestFixtures, type SuitePhaseHandler } from '@travetto/test';
3
+ import type { Class } from '@travetto/runtime';
5
4
  import { SchemaRegistryIndex } from '@travetto/schema';
5
+ import { type SuitePhaseHandler, SuiteRegistryIndex, TestFixtures } from '@travetto/test';
6
6
 
7
+ import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
7
8
  import { ModelBlobUtil } from '../../src/util/blob.ts';
8
9
  import { ModelStorageUtil } from '../../src/util/storage.ts';
9
- import { ModelRegistryIndex } from '../../src/registry/registry-index.ts';
10
10
 
11
- type ConfigType = { autoCreate?: boolean, namespace?: string };
11
+ type ConfigType = { autoCreate?: boolean; namespace?: string };
12
12
 
13
- class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass: Class }> implements SuitePhaseHandler<T> {
13
+ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>; serviceClass: Class }> implements SuitePhaseHandler<T> {
14
14
  qualifier?: symbol;
15
15
  target: Class<T>;
16
16
  constructor(target: Class<T>, qualifier?: symbol) {
@@ -35,9 +35,11 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
35
35
  if (ModelStorageUtil.isSupported(service)) {
36
36
  await service.createStorage();
37
37
  if (service.upsertModel) {
38
- await Promise.all(ModelRegistryIndex.getClasses()
39
- .filter(cls => cls === SchemaRegistryIndex.getBaseClass(cls))
40
- .map(modelCls => service.upsertModel!(modelCls)));
38
+ await Promise.all(
39
+ ModelRegistryIndex.getClasses()
40
+ .filter(cls => cls === SchemaRegistryIndex.getBaseClass(cls))
41
+ .map(modelCls => service.upsertModel!(modelCls))
42
+ );
41
43
  }
42
44
  }
43
45
  }
@@ -45,8 +47,7 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
45
47
  async afterEach(instance: T) {
46
48
  const service = await DependencyRegistryIndex.getInstance<T>(instance.serviceClass, this.qualifier);
47
49
  if (ModelStorageUtil.isSupported(service)) {
48
- const models = ModelRegistryIndex.getClasses()
49
- .filter(model => model === SchemaRegistryIndex.getBaseClass(model));
50
+ const models = ModelRegistryIndex.getClasses().filter(model => model === SchemaRegistryIndex.getBaseClass(model));
50
51
 
51
52
  if (ModelBlobUtil.isSupported(service) && service.truncateBlob) {
52
53
  await service.truncateBlob();
@@ -83,7 +84,9 @@ class ModelSuiteHandler<T extends { configClass: Class<ConfigType>, serviceClass
83
84
  * @example opt-in
84
85
  * @kind decorator
85
86
  */
86
- export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean, namespace?: string }>, serviceClass: Class }>(qualifier?: symbol) {
87
+ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean; namespace?: string }>; serviceClass: Class }>(
88
+ qualifier?: symbol
89
+ ) {
87
90
  const fixtures = new TestFixtures(['@travetto/model']);
88
91
  return (target: Class<T>): void => {
89
92
  target.prototype.fixtures = fixtures;
@@ -91,4 +94,4 @@ export function ModelSuite<T extends { configClass: Class<{ autoCreate?: boolean
91
94
  phaseHandlers: [new ModelSuiteHandler(target, qualifier)]
92
95
  });
93
96
  };
94
- }
97
+ }
@@ -1,43 +0,0 @@
1
- import type { Class, DeepPartial } from '@travetto/runtime';
2
-
3
- import type { ModelType, OptionalId } from '../types/model.ts';
4
- import type { ModelBasicSupport } from './basic.ts';
5
-
6
- /**
7
- * Support for simple indexed activity
8
- *
9
- * @concrete
10
- */
11
- export interface ModelIndexedSupport extends ModelBasicSupport {
12
- /**
13
- * Get entity by index as defined by fields of idx and the body fields
14
- * @param cls The type to search by
15
- * @param idx The index name to search against
16
- * @param body The payload of fields needed to search
17
- */
18
- getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T>;
19
-
20
- /**
21
- * Delete entity by index as defined by fields of idx and the body fields
22
- * @param cls The type to search by
23
- * @param idx The index name to search against
24
- * @param body The payload of fields needed to search
25
- */
26
- deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void>;
27
-
28
- /**
29
- * List entity by ranged index as defined by fields of idx and the body fields
30
- * @param cls The type to search by
31
- * @param idx The index name to search against
32
- * @param body The payload of fields needed to search
33
- */
34
- listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T>;
35
-
36
- /**
37
- * Upsert by index, allowing the index to act as a primary key
38
- * @param cls The type to create for
39
- * @param idx The index name to use
40
- * @param body The document to potentially store
41
- */
42
- upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T>;
43
- }
@@ -1,144 +0,0 @@
1
- import { castTo, type Class, type DeepPartial, hasFunction, TypedObject } from '@travetto/runtime';
2
-
3
- import { IndexNotSupported } from '../error/invalid-index.ts';
4
- import { NotFoundError } from '../error/not-found.ts';
5
- import type { IndexConfig } from '../registry/types.ts';
6
- import type { ModelCrudSupport } from '../types/crud.ts';
7
- import type { ModelIndexedSupport } from '../types/indexed.ts';
8
- import type { ModelType, OptionalId } from '../types/model.ts';
9
- import { ModelRegistryIndex } from '../registry/registry-index.ts';
10
-
11
- type ComputeConfig = {
12
- includeSortInFields?: boolean;
13
- emptyValue?: unknown;
14
- emptySortValue?: unknown;
15
- };
16
-
17
- type IndexFieldPart = { path: string[], value: (string | boolean | Date | number) };
18
- type IndexSortPart = { path: string[], dir: number, value: number | Date };
19
-
20
- const DEFAULT_SEP = '\u8203';
21
-
22
- /**
23
- * Utils for working with indexed model services
24
- */
25
- export class ModelIndexedUtil {
26
-
27
- /**
28
- * Type guard for determining if service supports indexed operation
29
- */
30
- static isSupported = hasFunction<ModelIndexedSupport>('getByIndex');
31
-
32
- /**
33
- * Compute flattened field to value mappings
34
- * @param cls Class to get info for
35
- * @param idx Index config
36
- * @param item Item to read values from
37
- */
38
- static computeIndexParts<T extends ModelType>(
39
- cls: Class<T>, idx: IndexConfig<T> | string, item: DeepPartial<T>, opts: ComputeConfig = {}
40
- ): { fields: IndexFieldPart[], sorted: IndexSortPart | undefined } {
41
- const config = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
42
- const sortField = config.type === 'sorted' ? config.fields.at(-1) : undefined;
43
-
44
- const fields: IndexFieldPart[] = [];
45
- let sortDirection: number = 0;
46
- let sorted: IndexSortPart | undefined;
47
-
48
- for (const field of config.fields) {
49
- let fieldRef: Record<string, unknown> = field;
50
- let itemRef: Record<string, unknown> = item;
51
- const parts = [];
52
-
53
- while (itemRef !== undefined && itemRef !== null) {
54
- const key = TypedObject.keys(fieldRef)[0];
55
- itemRef = castTo(itemRef[key]);
56
- parts.push(key);
57
- if (typeof fieldRef[key] === 'boolean' || typeof fieldRef[key] === 'number') {
58
- if (config.type === 'sorted') {
59
- sortDirection = fieldRef[key] === true ? 1 : fieldRef[key] === false ? 0 : fieldRef[key];
60
- }
61
- break; // At the bottom
62
- } else {
63
- fieldRef = castTo(fieldRef[key]);
64
- }
65
- }
66
- if (field === sortField) {
67
- sorted = { path: parts, dir: sortDirection, value: castTo(itemRef) };
68
- }
69
- if (itemRef === undefined || itemRef === null) {
70
- const empty = field === sortField ? opts.emptySortValue : opts.emptyValue;
71
- if (empty === undefined || empty === Error) {
72
- throw new IndexNotSupported(cls, config, `Missing field value for ${parts.join('.')}`);
73
- }
74
- } else {
75
- if (field !== sortField || (opts.includeSortInFields ?? true)) {
76
- fields.push({ path: parts, value: castTo(itemRef) });
77
- }
78
- }
79
- }
80
-
81
- return { fields, sorted };
82
- }
83
-
84
- /**
85
- * Project item via index
86
- * @param cls Type to get index for
87
- * @param idx Index config
88
- */
89
- static projectIndex<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string, item?: DeepPartial<T>, config?: ComputeConfig): Record<string, unknown> {
90
- const response: Record<string, unknown> = {};
91
- for (const { path, value } of this.computeIndexParts(cls, idx, item ?? {}, config).fields) {
92
- let sub: Record<string, unknown> = response;
93
- const all = path.slice(0);
94
- const last = all.pop()!;
95
- for (const part of all) {
96
- sub = castTo(sub[part] ??= {});
97
- }
98
- sub[last] = value;
99
- }
100
- return response;
101
- }
102
-
103
- /**
104
- * Compute index key as a single value
105
- * @param cls Class to get index for
106
- * @param idx Index config
107
- * @param item item to process
108
- */
109
- static computeIndexKey<T extends ModelType>(
110
- cls: Class<T>,
111
- idx: IndexConfig<T> | string,
112
- item: DeepPartial<T> = {},
113
- config?: ComputeConfig & { separator?: string }
114
- ): { type: string, key: string, sort?: number | Date } {
115
- const { fields, sorted } = this.computeIndexParts(cls, idx, item, { ...(config ?? {}), includeSortInFields: false });
116
- const key = fields.map(({ value }) => value).map(value => `${value}`).join(config?.separator ?? DEFAULT_SEP);
117
- const indexConfig = typeof idx === 'string' ? ModelRegistryIndex.getIndex(cls, idx) : idx;
118
- return !sorted ? { type: indexConfig.type, key } : { type: indexConfig.type, key, sort: sorted.value };
119
- }
120
-
121
- /**
122
- * Naive upsert by index
123
- * @param service
124
- * @param cls
125
- * @param idx
126
- * @param body
127
- */
128
- static async naiveUpsert<T extends ModelType>(
129
- service: ModelIndexedSupport & ModelCrudSupport,
130
- cls: Class<T>, idx: string, body: OptionalId<T>
131
- ): Promise<T> {
132
- try {
133
- const { id } = await service.getByIndex(cls, idx, castTo(body));
134
- body.id = id;
135
- return await service.update(cls, castTo(body));
136
- } catch (error) {
137
- if (error instanceof NotFoundError) {
138
- return await service.create(cls, body);
139
- } else {
140
- throw error;
141
- }
142
- }
143
- }
144
- }