@twin.org/entity-storage-connector-scylladb 0.0.1 → 0.0.2-next.10

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 CHANGED
@@ -13,7 +13,7 @@ npm install @twin.org/entity-storage-connector-scylladb
13
13
  The tests developed are functional tests and need an instance of ScyllaDB up and running. To run ScyllaDB locally:
14
14
 
15
15
  ```shell
16
- docker run -p 9042:9042 --name twin-entity-storage-scylla --hostname scylla -d scylladb/scylla:5.4.9 --smp 1
16
+ docker run -p 9500:9042 --name twin-entity-storage-scylladb --hostname scylla -d scylladb/scylla:5.4.9 --smp 1
17
17
  ```
18
18
 
19
19
  Afterwards you can run the tests as follows:
@@ -2,7 +2,6 @@
2
2
 
3
3
  var core = require('@twin.org/core');
4
4
  var entity = require('@twin.org/entity');
5
- var loggingModels = require('@twin.org/logging-models');
6
5
  var cassandraDriver = require('cassandra-driver');
7
6
 
8
7
  // Copyright 2024 IOTA Stiftung.
@@ -12,15 +11,14 @@ var cassandraDriver = require('cassandra-driver');
12
11
  */
13
12
  class AbstractScyllaDBConnector {
14
13
  /**
15
- * Limit the number of entities when finding.
16
- * @internal
14
+ * Runtime name for the class.
17
15
  */
18
- static PAGE_SIZE = 40;
16
+ static CLASS_NAME = "AbstractScyllaDBConnector";
19
17
  /**
20
- * Runtime name for the class.
18
+ * Limit the number of entities when finding.
21
19
  * @internal
22
20
  */
23
- CLASS_NAME;
21
+ static _DEFAULT_LIMIT = 40;
24
22
  /**
25
23
  * The name of the database table.
26
24
  * @internal
@@ -32,7 +30,7 @@ class AbstractScyllaDBConnector {
32
30
  */
33
31
  _config;
34
32
  /**
35
- * The logging connector.
33
+ * The logging component.
36
34
  * @internal
37
35
  */
38
36
  _logging;
@@ -49,22 +47,18 @@ class AbstractScyllaDBConnector {
49
47
  /**
50
48
  * Create a new instance of AbstractScyllaDBConnector.
51
49
  * @param options The options for the connector.
52
- * @param options.loggingConnectorType The type of logging connector to use, defaults to no logging.
50
+ * @param options.loggingComponentType The type of logging component to use, defaults to no logging.
53
51
  * @param options.entitySchema The name of the entity schema.
54
52
  * @param options.config The configuration for the connector.
55
- * @param className The name of the derived class.
56
- */
57
- constructor(options, className) {
58
- this.CLASS_NAME = className;
59
- core.Guards.object(this.CLASS_NAME, "options", options);
60
- core.Guards.stringValue(this.CLASS_NAME, "options.entitySchema", options.entitySchema);
61
- core.Guards.object(this.CLASS_NAME, "options.config", options.config);
62
- core.Guards.arrayValue(this.CLASS_NAME, "options.config.hosts", options.config.hosts);
63
- core.Guards.stringValue(this.CLASS_NAME, "options.config.localDataCenter", options.config.localDataCenter);
64
- core.Guards.stringValue(this.CLASS_NAME, "options.config.keyspace", options.config.keyspace);
65
- if (core.Is.stringValue(options.loggingConnectorType)) {
66
- this._logging = loggingModels.LoggingConnectorFactory.get(options.loggingConnectorType);
67
- }
53
+ */
54
+ constructor(options) {
55
+ core.Guards.object(AbstractScyllaDBConnector.CLASS_NAME, "options", options);
56
+ core.Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "options.entitySchema", options.entitySchema);
57
+ core.Guards.object(AbstractScyllaDBConnector.CLASS_NAME, "options.config", options.config);
58
+ core.Guards.arrayValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.hosts", options.config.hosts);
59
+ core.Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.localDataCenter", options.config.localDataCenter);
60
+ core.Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "options.config.keyspace", options.config.keyspace);
61
+ this._logging = core.ComponentFactory.getIfExists(options.loggingComponentType ?? "logging");
68
62
  this._entitySchema = entity.EntitySchemaFactory.get(options.entitySchema);
69
63
  this._primaryKey = entity.EntitySchemaHelper.getPrimaryKey(this._entitySchema);
70
64
  this._config = options.config;
@@ -85,7 +79,7 @@ class AbstractScyllaDBConnector {
85
79
  * @returns The object if it can be found or undefined.
86
80
  */
87
81
  async get(id, secondaryIndex, conditions) {
88
- core.Guards.stringValue(this.CLASS_NAME, "id", id);
82
+ core.Guards.stringValue(AbstractScyllaDBConnector.CLASS_NAME, "id", id);
89
83
  let connection;
90
84
  try {
91
85
  const indexField = secondaryIndex ?? this._primaryKey?.property;
@@ -95,7 +89,7 @@ class AbstractScyllaDBConnector {
95
89
  }
96
90
  await this._logging?.log({
97
91
  level: "info",
98
- source: this.CLASS_NAME,
92
+ source: AbstractScyllaDBConnector.CLASS_NAME,
99
93
  ts: Date.now(),
100
94
  message: "sql",
101
95
  data: { sql }
@@ -107,7 +101,7 @@ class AbstractScyllaDBConnector {
107
101
  }
108
102
  }
109
103
  catch (error) {
110
- throw new core.GeneralError(this.CLASS_NAME, "getFailed", {
104
+ throw new core.GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "getFailed", {
111
105
  id
112
106
  }, error);
113
107
  }
@@ -120,15 +114,15 @@ class AbstractScyllaDBConnector {
120
114
  * @param conditions The conditions to match for the entities.
121
115
  * @param sortProperties The optional sort order.
122
116
  * @param properties The optional properties to return, defaults to all.
123
- * @param cursor The cursor to request the next page of entities.
124
- * @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
117
+ * @param cursor The cursor to request the next chunk of entities.
118
+ * @param limit The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
125
119
  * @returns All the entities for the storage matching the conditions,
126
120
  * and a cursor which can be used to request more entities.
127
121
  */
128
- async query(conditions, sortProperties, properties, cursor, pageSize) {
122
+ async query(conditions, sortProperties, properties, cursor, limit) {
129
123
  let connection;
130
124
  try {
131
- let returnSize = pageSize ?? AbstractScyllaDBConnector.PAGE_SIZE;
125
+ let returnSize = limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT;
132
126
  let sql = `SELECT * FROM "${this._fullTableName}"`;
133
127
  if (core.Is.array(properties)) {
134
128
  const fields = [];
@@ -220,7 +214,7 @@ class AbstractScyllaDBConnector {
220
214
  }
221
215
  await this._logging?.log({
222
216
  level: "info",
223
- source: this.CLASS_NAME,
217
+ source: AbstractScyllaDBConnector.CLASS_NAME,
224
218
  ts: Date.now(),
225
219
  message: "sql",
226
220
  data: { sql }
@@ -236,7 +230,7 @@ class AbstractScyllaDBConnector {
236
230
  };
237
231
  }
238
232
  catch (error) {
239
- throw new core.GeneralError(this.CLASS_NAME, "findFailed", { table: this._fullTableName }, error);
233
+ throw new core.GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "findFailed", { table: this._fullTableName }, error);
240
234
  }
241
235
  finally {
242
236
  await this.closeConnection(connection);
@@ -253,7 +247,10 @@ class AbstractScyllaDBConnector {
253
247
  const client = new cassandraDriver.Client({
254
248
  contactPoints: this._config.hosts,
255
249
  localDataCenter: this._config.localDataCenter,
256
- keyspace: skipKeySpace ? undefined : this._config.keyspace
250
+ keyspace: skipKeySpace ? undefined : this._config.keyspace,
251
+ protocolOptions: {
252
+ port: this._config.port
253
+ }
257
254
  });
258
255
  await client.connect();
259
256
  return client;
@@ -278,13 +275,13 @@ class AbstractScyllaDBConnector {
278
275
  * @returns The rows.
279
276
  * @internal
280
277
  */
281
- async queryDB(connection, sql, params, pageState, pageSize) {
278
+ async queryDB(connection, sql, params, pageState, limit) {
282
279
  return new Promise((resolve, reject) => {
283
280
  const rows = [];
284
281
  connection.eachRow(sql, params, {
285
282
  prepare: true,
286
283
  autoPage: false,
287
- fetchSize: pageSize ?? AbstractScyllaDBConnector.PAGE_SIZE,
284
+ fetchSize: limit ?? AbstractScyllaDBConnector._DEFAULT_LIMIT,
288
285
  pageState
289
286
  }, (n, row) => {
290
287
  rows.push(row);
@@ -339,7 +336,7 @@ class AbstractScyllaDBConnector {
339
336
  return JSON.parse(value);
340
337
  }
341
338
  catch {
342
- throw new core.GeneralError(this.CLASS_NAME, "parseJSONFailed", {
339
+ throw new core.GeneralError(AbstractScyllaDBConnector.CLASS_NAME, "parseJSONFailed", {
343
340
  name: fieldDescriptor.property,
344
341
  value
345
342
  });
@@ -359,7 +356,6 @@ class AbstractScyllaDBConnector {
359
356
  */
360
357
  propertyToDbValue(value, fieldDescriptor) {
361
358
  if (fieldDescriptor) {
362
- // eslint-disable-next-line no-constant-condition
363
359
  if (fieldDescriptor.type === "string" && fieldDescriptor.format === "json") {
364
360
  return core.Is.empty(value) ? "null" : this.jsonWrap(value);
365
361
  }
@@ -408,7 +404,6 @@ class AbstractScyllaDBConnector {
408
404
  */
409
405
  jsonWrap(value) {
410
406
  let json = JSON.stringify(value);
411
- // eslint-disable-next-line no-control-regex
412
407
  json = json.replace(/[\b\0\t\n\r\u001A\\]/g, s => {
413
408
  switch (s) {
414
409
  case "\0":
@@ -440,24 +435,25 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
440
435
  /**
441
436
  * Runtime name for the class.
442
437
  */
443
- CLASS_NAME = "ScyllaDBTableConnector";
438
+ static CLASS_NAME = "ScyllaDBTableConnector";
444
439
  /**
445
440
  * Create a new instance of ScyllaDBTableConnector.
446
441
  * @param options The options for the connector.
447
442
  */
443
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
448
444
  constructor(options) {
449
- super(options, "ScyllaDBTableConnector");
445
+ super(options);
450
446
  }
451
447
  /**
452
448
  * Bootstrap the component by creating and initializing any resources it needs.
453
- * @param nodeLoggingConnectorType The node logging connector type, defaults to "node-logging".
449
+ * @param nodeLoggingComponentType The node logging component type.
454
450
  * @returns True if the bootstrapping process was successful.
455
451
  */
456
- async bootstrap(nodeLoggingConnectorType) {
457
- const nodeLogging = loggingModels.LoggingConnectorFactory.getIfExists(nodeLoggingConnectorType ?? "node-logging");
452
+ async bootstrap(nodeLoggingComponentType) {
453
+ const nodeLogging = core.ComponentFactory.getIfExists(nodeLoggingComponentType);
458
454
  nodeLogging?.log({
459
455
  level: "info",
460
- source: this.CLASS_NAME,
456
+ source: ScyllaDBTableConnector.CLASS_NAME,
461
457
  ts: Date.now(),
462
458
  message: "tableCreating",
463
459
  data: { table: this._fullTableName }
@@ -485,7 +481,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
485
481
  "${subTypeSchemaRef}" (${typeFields.join(",")})`;
486
482
  await nodeLogging?.log({
487
483
  level: "info",
488
- source: this.CLASS_NAME,
484
+ source: ScyllaDBTableConnector.CLASS_NAME,
489
485
  ts: Date.now(),
490
486
  message: "sql",
491
487
  data: { sql }
@@ -493,7 +489,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
493
489
  await this.execute(dbConnection, sql);
494
490
  await nodeLogging?.log({
495
491
  level: "info",
496
- source: this.CLASS_NAME,
492
+ source: ScyllaDBTableConnector.CLASS_NAME,
497
493
  ts: Date.now(),
498
494
  message: "typeCreated",
499
495
  data: { typeName: subTypeSchemaRef }
@@ -523,7 +519,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
523
519
  const sql = `CREATE TABLE IF NOT EXISTS "${this._fullTableName}" (${fields.join(", ")})`;
524
520
  await nodeLogging?.log({
525
521
  level: "info",
526
- source: this.CLASS_NAME,
522
+ source: ScyllaDBTableConnector.CLASS_NAME,
527
523
  ts: Date.now(),
528
524
  message: "sql",
529
525
  data: { sql }
@@ -531,7 +527,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
531
527
  await this.execute(dbConnection, sql);
532
528
  await nodeLogging?.log({
533
529
  level: "info",
534
- source: this.CLASS_NAME,
530
+ source: ScyllaDBTableConnector.CLASS_NAME,
535
531
  ts: Date.now(),
536
532
  message: "tableCreated",
537
533
  data: { table: this._fullTableName }
@@ -541,7 +537,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
541
537
  if (core.BaseError.isErrorCode(err, "ResourceInUseException")) {
542
538
  await nodeLogging?.log({
543
539
  level: "info",
544
- source: this.CLASS_NAME,
540
+ source: ScyllaDBTableConnector.CLASS_NAME,
545
541
  ts: Date.now(),
546
542
  message: "tableExists",
547
543
  data: { table: this._fullTableName }
@@ -550,7 +546,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
550
546
  else {
551
547
  await nodeLogging?.log({
552
548
  level: "error",
553
- source: this.CLASS_NAME,
549
+ source: ScyllaDBTableConnector.CLASS_NAME,
554
550
  ts: Date.now(),
555
551
  message: "tableCreateFailed",
556
552
  error: err,
@@ -567,7 +563,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
567
563
  * @param conditions The optional conditions to match for the entities.
568
564
  */
569
565
  async set(entity$1, conditions) {
570
- core.Guards.object(this.CLASS_NAME, "entity", entity$1);
566
+ core.Guards.object(ScyllaDBTableConnector.CLASS_NAME, "entity", entity$1);
571
567
  entity.EntitySchemaHelper.validateEntity(entity$1, this.getSchema());
572
568
  let connection;
573
569
  const id = entity$1[this._primaryKey?.property];
@@ -596,7 +592,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
596
592
  const sql = `UPDATE "${this._fullTableName}" SET ${updateValues.join(",")}${conditionString}`;
597
593
  await this._logging?.log({
598
594
  level: "info",
599
- source: this.CLASS_NAME,
595
+ source: ScyllaDBTableConnector.CLASS_NAME,
600
596
  ts: Date.now(),
601
597
  message: "sql",
602
598
  data: { sql }
@@ -605,7 +601,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
605
601
  await this.execute(connection, sql, propValues);
606
602
  }
607
603
  catch (error) {
608
- throw new core.GeneralError(this.CLASS_NAME, "entityStorage.setFailed", {
604
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "setFailed", {
609
605
  id
610
606
  }, error);
611
607
  }
@@ -619,7 +615,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
619
615
  * @param conditions The optional conditions to match for the entities.
620
616
  */
621
617
  async remove(id, conditions) {
622
- core.Guards.stringValue(this.CLASS_NAME, "id", id);
618
+ core.Guards.stringValue(ScyllaDBTableConnector.CLASS_NAME, "id", id);
623
619
  let connection;
624
620
  try {
625
621
  conditions ??= [];
@@ -628,16 +624,16 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
628
624
  const sql = `DELETE FROM "${this._fullTableName}" WHERE ${sqlCondition}`;
629
625
  await this._logging?.log({
630
626
  level: "info",
631
- source: this.CLASS_NAME,
627
+ source: ScyllaDBTableConnector.CLASS_NAME,
632
628
  ts: Date.now(),
633
- message: "entityStorage.sqlRemove",
629
+ message: "sql",
634
630
  data: { sql }
635
631
  });
636
632
  connection = await this.openConnection();
637
633
  await this.execute(connection, sql, conditionValues);
638
634
  }
639
635
  catch (error) {
640
- throw new core.GeneralError(this.CLASS_NAME, "removeFailed", {
636
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "removeFailed", {
641
637
  id
642
638
  }, error);
643
639
  }
@@ -655,7 +651,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
655
651
  await connection.execute(`DROP TABLE IF EXISTS "${this._fullTableName}"`);
656
652
  }
657
653
  catch (error) {
658
- throw new core.GeneralError(this.CLASS_NAME, "dropTableFailed", { table: this._fullTableName }, error);
654
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "dropTableFailed", { table: this._fullTableName }, error);
659
655
  }
660
656
  finally {
661
657
  await this.closeConnection(connection);
@@ -671,7 +667,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
671
667
  await connection.execute(`TRUNCATE TABLE "${this._fullTableName}"`);
672
668
  }
673
669
  catch (error) {
674
- throw new core.GeneralError(this.CLASS_NAME, "truncateTableFailed", { table: this._fullTableName }, error);
670
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "truncateTableFailed", { table: this._fullTableName }, error);
675
671
  }
676
672
  finally {
677
673
  await this.closeConnection(connection);
@@ -736,7 +732,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
736
732
  break;
737
733
  case "object":
738
734
  if (!logicalField.itemTypeRef) {
739
- throw new core.GeneralError(this.CLASS_NAME, "itemTypeNotDefined", {
735
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "itemTypeNotDefined", {
740
736
  type: logicalField.type,
741
737
  table: this._fullTableName
742
738
  });
@@ -745,7 +741,7 @@ class ScyllaDBTableConnector extends AbstractScyllaDBConnector {
745
741
  break;
746
742
  case "array":
747
743
  if (!logicalField.itemType && !logicalField.itemTypeRef) {
748
- throw new core.GeneralError(this.CLASS_NAME, "itemTypeNotDefined", {
744
+ throw new core.GeneralError(ScyllaDBTableConnector.CLASS_NAME, "itemTypeNotDefined", {
749
745
  type: logicalField.type,
750
746
  table: this._fullTableName
751
747
  });
@@ -791,7 +787,7 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
791
787
  /**
792
788
  * Runtime name for the class.
793
789
  */
794
- CLASS_NAME = "ScyllaDBViewConnector";
790
+ static CLASS_NAME = "ScyllaDBViewConnector";
795
791
  /**
796
792
  * The view descriptor.
797
793
  * @internal
@@ -809,10 +805,10 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
809
805
  constructor(options) {
810
806
  // We need this conversion so that types can match in the superclass and reuse the get method
811
807
  super({
812
- loggingConnectorType: options.loggingConnectorType,
808
+ loggingComponentType: options.loggingComponentType,
813
809
  entitySchema: options.viewSchema,
814
810
  config: options.config
815
- }, "ScyllaDBViewConnector");
811
+ });
816
812
  this._viewSchema = entity.EntitySchemaHelper.getSchema(options.viewSchema);
817
813
  // We need the underlying class to use the view name for lookups
818
814
  // so substitute the view name for the entity name
@@ -822,14 +818,14 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
822
818
  }
823
819
  /**
824
820
  * Bootstrap the component by creating and initializing any resources it needs.
825
- * @param nodeLoggingConnectorType The node logging connector type, defaults to "node-logging".
821
+ * @param nodeLoggingComponentType The node logging component type.
826
822
  * @returns True if the bootstrapping process was successful.
827
823
  */
828
- async bootstrap(nodeLoggingConnectorType) {
829
- const nodeLogging = loggingModels.LoggingConnectorFactory.getIfExists(nodeLoggingConnectorType ?? "node-logging");
824
+ async bootstrap(nodeLoggingComponentType) {
825
+ const nodeLogging = core.ComponentFactory.getIfExists(nodeLoggingComponentType);
830
826
  nodeLogging?.log({
831
827
  level: "info",
832
- source: this.CLASS_NAME,
828
+ source: ScyllaDBViewConnector.CLASS_NAME,
833
829
  ts: Date.now(),
834
830
  message: "viewCreating",
835
831
  data: { view: this._fullTableName }
@@ -852,7 +848,7 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
852
848
  await this.execute(dbConnection, sql);
853
849
  nodeLogging?.log({
854
850
  level: "info",
855
- source: this.CLASS_NAME,
851
+ source: ScyllaDBViewConnector.CLASS_NAME,
856
852
  ts: Date.now(),
857
853
  message: "viewCreated",
858
854
  data: { view: this._fullTableName }
@@ -862,7 +858,7 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
862
858
  if (core.BaseError.isErrorCode(err, "ResourceInUseException")) {
863
859
  nodeLogging?.log({
864
860
  level: "info",
865
- source: this.CLASS_NAME,
861
+ source: ScyllaDBViewConnector.CLASS_NAME,
866
862
  ts: Date.now(),
867
863
  message: "viewExists",
868
864
  data: { view: this._fullTableName }
@@ -871,7 +867,7 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
871
867
  else {
872
868
  nodeLogging?.log({
873
869
  level: "error",
874
- source: this.CLASS_NAME,
870
+ source: ScyllaDBViewConnector.CLASS_NAME,
875
871
  ts: Date.now(),
876
872
  message: "viewCreateFailed",
877
873
  error: err,
@@ -887,14 +883,18 @@ class ScyllaDBViewConnector extends AbstractScyllaDBConnector {
887
883
  * @param entity The entity to set.
888
884
  */
889
885
  async set(entity) {
890
- throw new core.NotSupportedError(this.CLASS_NAME, "set", {});
886
+ throw new core.NotSupportedError(ScyllaDBViewConnector.CLASS_NAME, "notSupported", {
887
+ methodName: "set"
888
+ });
891
889
  }
892
890
  /**
893
891
  * Delete the entity.
894
892
  * @param id The id of the entity to remove.
895
893
  */
896
894
  async remove(id) {
897
- throw new core.NotSupportedError(this.CLASS_NAME, "remove", {});
895
+ throw new core.NotSupportedError(ScyllaDBViewConnector.CLASS_NAME, "notSupported", {
896
+ methodName: "remove"
897
+ });
898
898
  }
899
899
  }
900
900