@lossless.org/client 0.1.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/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
package/readme.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @lossless.org/client
|
|
2
|
+
|
|
3
|
+
One TypeScript client for lossless.org NoSQLDB, SQLDB and ObjectStorage, with MongoDB, MariaDB, ClickHouse and S3 protocol adapters. `LosslessOrgClient` owns named connections and provides `nosqldb()`, `sqldb()` and `objectstorage()` interfaces. Server engines remain separate packages; this client never starts a database or creates a bucket implicitly.
|
|
4
|
+
|
|
5
|
+
## Issue Reporting and Security
|
|
6
|
+
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
8
|
+
|
|
9
|
+
## Install and connect
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @lossless.org/client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { LosslessOrgClient } from '@lossless.org/client';
|
|
17
|
+
|
|
18
|
+
const client = new LosslessOrgClient({
|
|
19
|
+
nosqldb: {
|
|
20
|
+
app: { backend: 'mongodb', url: process.env.MONGODB_URL!, database: 'app' },
|
|
21
|
+
},
|
|
22
|
+
sqldb: {
|
|
23
|
+
primary: { backend: 'mariadb', host: process.env.SQL_HOST!, database: 'app',
|
|
24
|
+
user: process.env.SQL_USER!, password: process.env.SQL_PASSWORD! },
|
|
25
|
+
analytics: { backend: 'clickhouse', url: process.env.CLICKHOUSE_URL!, database: 'app',
|
|
26
|
+
username: process.env.CLICKHOUSE_USER!, password: process.env.CLICKHOUSE_PASSWORD! },
|
|
27
|
+
},
|
|
28
|
+
objectstorage: {
|
|
29
|
+
assets: { backend: 's3', endpoint: process.env.S3_ENDPOINT!, region: 'us-east-1',
|
|
30
|
+
credentials: { accessKeyId: process.env.S3_ACCESS_KEY!, secretAccessKey: process.env.S3_SECRET_KEY! },
|
|
31
|
+
readinessBucket: 'existing-assets' },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await client.connect({ timeoutMs: 30_000 });
|
|
36
|
+
try {
|
|
37
|
+
const primary = client.sqldb('primary'); // SqlConnection
|
|
38
|
+
const analytics = client.sqldb('analytics'); // ClickHouseConnection
|
|
39
|
+
const rows = await primary.query<{ id: bigint }>({
|
|
40
|
+
sql: 'SELECT id FROM accounts WHERE email = ?', values: ['alice@example.com'],
|
|
41
|
+
}, { maxRows: 100 });
|
|
42
|
+
for await (const row of analytics.stream<{ temperature: number }>({
|
|
43
|
+
sql: 'SELECT temperature FROM readings WHERE device = {device:String}',
|
|
44
|
+
values: { device: 'ssd-1' },
|
|
45
|
+
})) {
|
|
46
|
+
// Process each row before requesting more; filtering happens on the server.
|
|
47
|
+
}
|
|
48
|
+
const readiness = await client.ready();
|
|
49
|
+
} finally {
|
|
50
|
+
await client.close();
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Select `nosqldb`, `sqldb` or `objectstorage` as the backend for the corresponding lossless.org server. An engine profile declares capabilities; it does not translate unsupported SQL or add backend features. SQLDB requires engine version 0.2.3 or later for pooled connection reset and parameterless prepared statements. It has a smaller SQL/type subset than MariaDB, and does not currently support TLS, savepoints or `ALTER TABLE`. ClickHouse uses its HTTP protocol and SQL dialect. There are no cross-backend transactions or automatic replication.
|
|
55
|
+
|
|
56
|
+
Literal connection names and backend discriminators determine the return types. Each client owns the connections it constructs. `connect()` probes databases without changing their schemas. S3 construction is local; `ready()` checks an explicitly configured existing bucket and returns `readiness_bucket_required` if none was supplied. `close()` is idempotent, stops admission, cancels owned SQL operations, closes pools and joins final metrics-writer flushes. Create a new aggregate client after failure or close.
|
|
57
|
+
|
|
58
|
+
## Interfaces
|
|
59
|
+
|
|
60
|
+
| Import | API |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `@lossless.org/client` | `LosslessOrgClient`, configuration, capabilities, readiness, `LosslessClientError` |
|
|
63
|
+
| `@lossless.org/client/nosqldb` | Migrated SmartData models, decorators, collections, cursors, sessions, exact persistence and administration; `NoSqlConnection` |
|
|
64
|
+
| `@lossless.org/client/sqldb` | `SqlConnection`, `SqlTransaction`, `SqlTable`, `ClickHouseConnection`, `SmartClickHouseDb`, tables, query builders, `TimeDataTable`, `MetricWriter` |
|
|
65
|
+
| `@lossless.org/client/objectstorage` | Migrated `SmartBucket`, buckets, directories, files, metadata, watchers, exact operations; `ObjectStorageConnection` |
|
|
66
|
+
| `@lossless.org/client/testsupport` | Explicit disposable-database testing helpers |
|
|
67
|
+
|
|
68
|
+
Family imports preserve the established SmartData and SmartBucket names and constructors. Their implementations live here; the client does not depend on the old packages. The aggregate loads only configured families. Direct family entry points do not initialize unrelated connections.
|
|
69
|
+
|
|
70
|
+
## Relational SQL
|
|
71
|
+
|
|
72
|
+
`SqlConnection` uses the official MariaDB connector for parameters, protocol, TLS and decoding. This client owns cancellable admission and TCP sockets from authentication onward. A transaction holds one physical session; releasing a session resets its state before reuse.
|
|
73
|
+
|
|
74
|
+
- `execute({ sql, values }, options)` returns `affectedRows`, optional exact `insertId: bigint`, and `completion: 'acknowledged'`. Use it for statements that return an update result.
|
|
75
|
+
- `query<Row>(statement, options)` materializes at most 10,000 rows and 16 MiB by default. Set `maxRows` and `maxBytes` explicitly to change those limits. Overflow rejects with `result_limit` and closes the stream.
|
|
76
|
+
- `stream<Row>(statement, options)` iterates rows with backpressure. Early return, timeout, cancellation and connection shutdown close its owned stream/socket.
|
|
77
|
+
- `insert(table, iterable, options)` accepts iterable or async-iterable rows with the same columns, batching at 500 rows/4 MiB by default. Batches commit independently. A later failure reports `partial_write` with acknowledged rows, or `ambiguous_write` if the current batch outcome is unknown. Use a transaction with explicit statements for atomic multi-statement writes.
|
|
78
|
+
- `transaction(async tx => ..., options)` joins an outstanding final transaction operation before commit. Await each operation; concurrent operations on one transaction are rejected. A thrown callback rolls back when the session is still connected. An interrupted commit has an unknown outcome.
|
|
79
|
+
- `table<Row>(name)` handles an existing table with bound equality/null selectors, typed `query`, `stream`, `insert`, `update` and `delete`. Empty mutation selectors and `undefined` selector values are rejected. Schema creation is explicit SQL.
|
|
80
|
+
|
|
81
|
+
Values use placeholders; identifiers use a separate quoting function. Raw SQL text is trusted application code. A result generic is a caller-declared shape, not static validation of arbitrary SQL.
|
|
82
|
+
|
|
83
|
+
MariaDB returns `BIGINT` as `bigint`, `DECIMAL` as a string, binary columns as `Buffer`, SQL null as `null`, and dates/times as strings. Fractional timestamp strings retain server precision. JavaScript `Date` inputs bind in UTC with millisecond precision; MariaDB sessions use UTC. Exact fractional values beyond milliseconds should be supplied as strings. Unsafe integer numbers, non-finite numbers, invalid dates and unsupported parameter objects are rejected; use `bigint` or decimal strings for exact large values. SQLDB's supported scalar types follow its engine contract.
|
|
84
|
+
|
|
85
|
+
## ClickHouse and metrics
|
|
86
|
+
|
|
87
|
+
`ClickHouseConnection` uses the official streaming HTTP connector. `stream()` consumes `JSONEachRow` batches without buffering the complete response. `query()` applies the same materialization limits as relational SQL. `insert()` streams an iterable with a default 4 MiB per-row limit and backpressure. It does not make a large insert atomic; a failed insert may have stored part or all of the input. JavaScript JSON row values must be serializable; represent 64-bit input integers as decimal strings. `Int64`/`UInt64` and decimal results are returned as strings; decimal formatting follows the server and may omit trailing zeros. Binary data needs an explicit application encoding. Date/time strings follow the column's ClickHouse type and timezone.
|
|
88
|
+
|
|
89
|
+
`execute()` waits for server response completion and requests synchronous mutations, returning a query ID and acknowledged completion. It does not invent relational transactions for ClickHouse. Named parameters use ClickHouse syntax, such as `{device:String}`. Identifiers are quoted separately, including literal dots and backslashes.
|
|
90
|
+
|
|
91
|
+
`connection.metrics` exposes the migrated table/query features. Canonical `metrics.createTable()` defaults `autoSchemaEvolution` to false; preparation and schema changes are explicit. Standalone `SmartClickHouseDb` retains its existing opt-in startup/schema behavior for migrating applications.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const table = await client.sqldb('analytics').metrics.createTable<{ id: number; temperature: number }>({
|
|
95
|
+
tableName: 'readings', orderBy: 'id',
|
|
96
|
+
columns: [{ name: 'id', type: 'UInt32' }, { name: 'temperature', type: 'Float64' }],
|
|
97
|
+
});
|
|
98
|
+
const writer = table.createInsertStream({ batchSize: 500, maxBatchBytes: 4 * 1024 * 1024 });
|
|
99
|
+
await writer.write({ id: 1, temperature: 38.5 });
|
|
100
|
+
await writer.close(); // Joins the final server acknowledgement; rejects on failure.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Await every writer `write()` for backpressure. `flush()` and `close()` propagate errors, including background flush failures. Client close flushes owned writers before closing the connection. Timestamp-only polling watches cannot guarantee delivery of equal-timestamp or late rows and explicitly report `unsupported_capability`.
|
|
104
|
+
|
|
105
|
+
SQLDB advanced object metrics and disk-resident analytical scans remain unavailable. The client does not download objects to discover nested paths or aggregate them. Automatic deep-path indexing, correlated array predicates, expiry-driven path retirement and terabyte-scale storage require the corresponding qualified engine capability.
|
|
106
|
+
|
|
107
|
+
## Budgets, errors and capabilities
|
|
108
|
+
|
|
109
|
+
New SQL methods accept `signal` and a total `timeoutMs` (30 seconds by default, including pool admission). Read cancellation closes owned transport resources. User-provided iterators receive `return()` on cancellation; their own pending I/O must also cooperate with the caller's signal. JavaScript cannot forcibly interrupt an arbitrary promise or transaction callback. Expiry revokes the transaction handle, so a callback resuming later cannot dispatch a write.
|
|
110
|
+
|
|
111
|
+
`LosslessClientError` provides a safe outer message, a code, optional `backendCode`, `retryability`, `outcome` and acknowledged-row evidence. `cause` preserves the original error and can contain statement values; do not log it indiscriminately. The taxonomy includes invalid arguments, unsupported capability, authentication, conflict, timeout, cancellation, result limits, backend failure, partial writes and unknown write outcomes. There are no automatic write retries. A transient hint never establishes that retrying an ambiguous write is safe.
|
|
112
|
+
|
|
113
|
+
Migrated document and object APIs retain their existing typed errors and exact-operation evidence. Their established per-operation timeout/ownership contracts continue to apply. `capabilities` distinguishes `available`, `unsupported` and `unknown`; MongoDB transaction/change-stream availability still depends on deployment topology. NoSQLDB change streams are explicitly unsupported. S3 exact-operation capability tests remain explicit and require owned disposable resources.
|
|
114
|
+
|
|
115
|
+
## Migrating existing applications
|
|
116
|
+
|
|
117
|
+
Replace imports and the corresponding manifest dependency:
|
|
118
|
+
|
|
119
|
+
| Previous package | New import |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `@push.rocks/smartdata` | `@lossless.org/client/nosqldb` |
|
|
122
|
+
| `@push.rocks/smartdata/testsupport` | `@lossless.org/client/testsupport` |
|
|
123
|
+
| `@push.rocks/smartbucket` | `@lossless.org/client/objectstorage` |
|
|
124
|
+
| `@push.rocks/smartclickhouse` | `@lossless.org/client/sqldb` |
|
|
125
|
+
|
|
126
|
+
SmartData persisted identities, decorator symbols, BSON/null/undefined behavior and exact persistence contracts are retained. Existing normal application access stays through those public model APIs; direct driver access belongs to this foundation or explicit versioned migrations.
|
|
127
|
+
|
|
128
|
+
ObjectStorage retains companion `<key>.metadata` objects and `.trash/<encoded-original-key>` layouts. Existing buffer/replay helpers, list-array methods and watcher state materialize data and are outside the new streaming-memory guarantee. Metadata locks are advisory, not atomic distributed locks; directory move remains unsupported. The `getStorageClient()` migration escape hatch is retained. No readiness call creates or deletes a bucket.
|
|
129
|
+
|
|
130
|
+
SmartClickHouse migration changes: replace RxJS `next/complete` insertion with awaited writer `write/close`; join database close during application shutdown. Materialized SQL reads now have explicit limits. Query-builder `toSQL()` contains placeholders; use `toStatement()` or pass its `parameters` with the SQL. Watch APIs reject the unsupported continuation guarantee. Insert errors always reach the caller. Existing stored dotted-column names remain unchanged. Original API documentation and licenses are preserved in the repository under [docs/source](https://code.foss.global/lossless.org/client/src/branch/main/docs/source).
|
|
131
|
+
|
|
132
|
+
## Verification
|
|
133
|
+
|
|
134
|
+
`pnpm build` builds production declarations. `pnpm test` checks aggregate lifecycle and writers. `pnpm run test:nosqldb`, `test:mongodb`, `test:objectstorage` and `test:sqldb` exercise isolated backends. Wrappers own and remove their disposable servers, containers and data directories. Never supply a production database or bucket to these destructive suites.
|
|
135
|
+
|
|
136
|
+
Qualification includes NoSQLDB 8.0.2, MongoDB 8.0.26 replica sets, ObjectStorage 10.1.0, SQLDB 0.2.3, MariaDB 11.8 and ClickHouse 25.8. `pnpm run test:minio` uses a digest-pinned MinIO image: `sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`. That MinIO version enforces the tested conditional uploads but does not enforce conditional deletion; its exact-purge capability is correctly unavailable. ObjectStorage passes both live probes. AWS S3 has not been qualified against a live account in this migration; no universal S3 exact-operation guarantee is inferred from the SDK.
|
|
137
|
+
|
|
138
|
+
## License and Legal Information
|
|
139
|
+
|
|
140
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository [license file](./license.md).
|
|
141
|
+
|
|
142
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
### Trademarks
|
|
145
|
+
|
|
146
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
147
|
+
|
|
148
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
149
|
+
|
|
150
|
+
### Company Information
|
|
151
|
+
|
|
152
|
+
Task Venture Capital GmbH<br>
|
|
153
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
154
|
+
|
|
155
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
156
|
+
|
|
157
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { NoSqlConnection, INoSqlConnectionOptions } from './nosqldb/classes.connection.js';
|
|
3
|
+
import type { ObjectStorageConnection, IObjectStorageConnectionOptions } from './objectstorage/classes.connection.js';
|
|
4
|
+
import type { SqlConnection } from './sqldb/classes.sqlconnection.js';
|
|
5
|
+
import type { ClickHouseConnection } from './sqldb/classes.clickhouseconnection.js';
|
|
6
|
+
import type { ISqlConnectionOptions, IClickHouseConnectionOptions } from './sqldb/interfaces.js';
|
|
7
|
+
import type { IOperationOptions, IReadiness } from './core/interfaces.js';
|
|
8
|
+
import { LosslessClientError } from './core/classes.error.js';
|
|
9
|
+
import { Operation } from './core/classes.operation.js';
|
|
10
|
+
|
|
11
|
+
export interface ILosslessOrgClientOptions {
|
|
12
|
+
nosqldb?: Record<string, INoSqlConnectionOptions>;
|
|
13
|
+
sqldb?: Record<string, ISqlConnectionOptions | IClickHouseConnectionOptions>;
|
|
14
|
+
objectstorage?: Record<string, IObjectStorageConnectionOptions>;
|
|
15
|
+
}
|
|
16
|
+
type TSqlFor<Options> = Options extends { backend: 'clickhouse' } ? ClickHouseConnection : SqlConnection;
|
|
17
|
+
type TResource = NoSqlConnection | ObjectStorageConnection | SqlConnection | ClickHouseConnection;
|
|
18
|
+
|
|
19
|
+
/** Named connections with one lifecycle; engines and persisted domain semantics stay separate. */
|
|
20
|
+
export class LosslessOrgClient<const Options extends ILosslessOrgClientOptions = ILosslessOrgClientOptions> {
|
|
21
|
+
private readonly resources = new Map<string, TResource>();
|
|
22
|
+
private connecting?: Promise<void>;
|
|
23
|
+
private closing?: Promise<void>;
|
|
24
|
+
private connectOperation?: Operation;
|
|
25
|
+
private state: 'initial' | 'connecting' | 'connected' | 'failed' | 'closing' | 'closed' = 'initial';
|
|
26
|
+
|
|
27
|
+
constructor(public readonly options: Options) {}
|
|
28
|
+
|
|
29
|
+
public connect(options: IOperationOptions = {}): Promise<void> {
|
|
30
|
+
if (this.state === 'connected') return Promise.resolve();
|
|
31
|
+
if (this.state === 'connecting' && this.connecting) return this.connecting;
|
|
32
|
+
if (this.state !== 'initial') return Promise.reject(new LosslessClientError('closed', 'Create a new client after failure or close.'));
|
|
33
|
+
this.connectOperation = new Operation(options);
|
|
34
|
+
this.state = 'connecting';
|
|
35
|
+
const operation = this.connectOperation;
|
|
36
|
+
this.connecting = (async () => {
|
|
37
|
+
try {
|
|
38
|
+
for (const [name, config] of Object.entries(this.options.nosqldb ?? {})) {
|
|
39
|
+
operation.check();
|
|
40
|
+
const { NoSqlConnection } = await plugins.loadNoSql();
|
|
41
|
+
const connection = new NoSqlConnection(config);
|
|
42
|
+
this.resources.set(`nosqldb:${name}`, connection);
|
|
43
|
+
await connection.connect({ signal: operation.signal });
|
|
44
|
+
}
|
|
45
|
+
for (const [name, config] of Object.entries(this.options.sqldb ?? {})) {
|
|
46
|
+
operation.check();
|
|
47
|
+
const connection = config.backend === 'clickhouse'
|
|
48
|
+
? new (await plugins.loadClickHouse()).ClickHouseConnection(config)
|
|
49
|
+
: new (await plugins.loadSql()).SqlConnection(config);
|
|
50
|
+
this.resources.set(`sqldb:${name}`, connection);
|
|
51
|
+
await connection.connect({ signal: operation.signal });
|
|
52
|
+
operation.check();
|
|
53
|
+
}
|
|
54
|
+
for (const [name, config] of Object.entries(this.options.objectstorage ?? {})) {
|
|
55
|
+
operation.check();
|
|
56
|
+
const { ObjectStorageConnection } = await plugins.loadObjects();
|
|
57
|
+
this.resources.set(`objectstorage:${name}`, new ObjectStorageConnection(config));
|
|
58
|
+
}
|
|
59
|
+
operation.check();
|
|
60
|
+
this.state = 'connected';
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const cleanup = await Promise.allSettled([...this.resources.values()].map(resource => resource.close()));
|
|
63
|
+
this.state = 'failed';
|
|
64
|
+
const errors = cleanup.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
65
|
+
if (errors.length) throw new AggregateError([error, ...errors], 'Client connection and cleanup failed.');
|
|
66
|
+
throw error;
|
|
67
|
+
} finally { operation.dispose(); this.connectOperation = undefined; }
|
|
68
|
+
})();
|
|
69
|
+
void this.connecting.finally(() => { this.connecting = undefined; }).catch(() => {});
|
|
70
|
+
return this.connecting;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private get(family: string, name: string): TResource {
|
|
74
|
+
if (this.state !== 'connected') throw new LosslessClientError('not_connected', 'Call and await client.connect() first.');
|
|
75
|
+
const resource = this.resources.get(`${family}:${name}`);
|
|
76
|
+
if (!resource) throw new LosslessClientError('invalid_argument', `Unknown ${family} connection: ${name}`);
|
|
77
|
+
return resource;
|
|
78
|
+
}
|
|
79
|
+
public nosqldb<Name extends keyof NonNullable<Options['nosqldb']> & string>(name: Name): NoSqlConnection {
|
|
80
|
+
return this.get('nosqldb', name) as NoSqlConnection;
|
|
81
|
+
}
|
|
82
|
+
public sqldb<Name extends keyof NonNullable<Options['sqldb']> & string>(name: Name): TSqlFor<NonNullable<Options['sqldb']>[Name]> {
|
|
83
|
+
return this.get('sqldb', name) as TSqlFor<NonNullable<Options['sqldb']>[Name]>;
|
|
84
|
+
}
|
|
85
|
+
public objectstorage<Name extends keyof NonNullable<Options['objectstorage']> & string>(name: Name): ObjectStorageConnection {
|
|
86
|
+
return this.get('objectstorage', name) as ObjectStorageConnection;
|
|
87
|
+
}
|
|
88
|
+
public async ready(options: IOperationOptions = {}): Promise<Record<string, IReadiness>> {
|
|
89
|
+
if (this.state !== 'connected') throw new LosslessClientError('not_connected', 'Client is not connected.');
|
|
90
|
+
return Object.fromEntries(await Promise.all([...this.resources].map(async ([name, resource]) => [name, await resource.ready(options)])));
|
|
91
|
+
}
|
|
92
|
+
public close(): Promise<void> {
|
|
93
|
+
return this.closing ??= (async () => {
|
|
94
|
+
this.state = 'closing';
|
|
95
|
+
this.connectOperation?.cancel();
|
|
96
|
+
await this.connecting?.catch(() => {});
|
|
97
|
+
const result = await Promise.allSettled([...this.resources.values()].map(resource => resource.close()));
|
|
98
|
+
const errors = result.flatMap(item => item.status === 'rejected' ? [item.reason] : []);
|
|
99
|
+
if (errors.length) throw new AggregateError(errors, 'Client cleanup failed.');
|
|
100
|
+
this.resources.clear();
|
|
101
|
+
this.state = 'closed';
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TClientErrorCode } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
export class LosslessClientError extends Error {
|
|
4
|
+
public readonly backendCode?: string;
|
|
5
|
+
/** A transient cause is a hint, never permission to retry an ambiguous mutation. */
|
|
6
|
+
public readonly retryability: 'transient' | 'permanent' | 'unknown';
|
|
7
|
+
public readonly outcome: 'unknown' | 'partial' | undefined;
|
|
8
|
+
public readonly acknowledgedRows?: number;
|
|
9
|
+
constructor(public readonly code: TClientErrorCode, message: string,
|
|
10
|
+
options?: ErrorOptions & { backendCode?: string; retryability?: 'transient' | 'permanent' | 'unknown'; acknowledgedRows?: number }) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = 'LosslessClientError';
|
|
13
|
+
this.backendCode = options?.backendCode;
|
|
14
|
+
this.retryability = options?.retryability ?? 'unknown';
|
|
15
|
+
this.outcome = code === 'ambiguous_write' ? 'unknown' : code === 'partial_write' ? 'partial' : undefined;
|
|
16
|
+
this.acknowledgedRows = options?.acknowledgedRows;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Safe outer message; the original backend error remains available explicitly through cause. */
|
|
21
|
+
export const backendError = (error: unknown): LosslessClientError => {
|
|
22
|
+
if (error instanceof LosslessClientError) return error;
|
|
23
|
+
const details = error && typeof error === 'object' ? error as Record<string, unknown> : {};
|
|
24
|
+
const code = String(details.code ?? details.errno ?? '');
|
|
25
|
+
const authentication = ['ER_ACCESS_DENIED_ERROR', 'ER_DBACCESS_DENIED_ERROR', '1045', '1044', '516'].includes(code);
|
|
26
|
+
const conflict = ['ER_DUP_ENTRY', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT', '1062', '1213', '1205'].includes(code);
|
|
27
|
+
const transient = ['ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT', '1213', '1205', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT'].includes(code);
|
|
28
|
+
return new LosslessClientError(authentication ? 'authentication' : conflict ? 'conflict' : 'backend_error',
|
|
29
|
+
authentication ? 'Backend authentication failed.' : conflict ? 'Backend operation conflicted.' : 'Backend operation failed.',
|
|
30
|
+
{ cause: error, backendCode: code || undefined, retryability: transient ? 'transient' : authentication ? 'permanent' : 'unknown' });
|
|
31
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { LosslessClientError } from './classes.error.js';
|
|
2
|
+
import type { IOperationOptions } from './interfaces.js';
|
|
3
|
+
|
|
4
|
+
/** Owns one deadline and abort listener; callers join resource cleanup before disposal. */
|
|
5
|
+
export class Operation {
|
|
6
|
+
public readonly controller = new AbortController();
|
|
7
|
+
public get signal(): AbortSignal { return this.controller.signal; }
|
|
8
|
+
private readonly timer: ReturnType<typeof setTimeout>;
|
|
9
|
+
private readonly upstream?: AbortSignal;
|
|
10
|
+
private readonly onAbort = () => this.cancel(new LosslessClientError('cancelled', 'Operation cancelled.'));
|
|
11
|
+
|
|
12
|
+
constructor(options: IOperationOptions = {}, defaultTimeoutMs = 30_000) {
|
|
13
|
+
const timeout = options.timeoutMs ?? defaultTimeoutMs;
|
|
14
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 86_400_000) {
|
|
15
|
+
throw new LosslessClientError('invalid_argument', 'timeoutMs must be an integer between 1 and 86400000.');
|
|
16
|
+
}
|
|
17
|
+
this.upstream = options.signal;
|
|
18
|
+
this.upstream?.addEventListener('abort', this.onAbort, { once: true });
|
|
19
|
+
this.timer = setTimeout(() => this.cancel(new LosslessClientError('timeout', 'Operation deadline exceeded.')), timeout);
|
|
20
|
+
this.timer.unref();
|
|
21
|
+
if (this.upstream?.aborted) this.onAbort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public check(): void { if (this.signal.aborted) throw this.signal.reason; }
|
|
25
|
+
/** Revoke admission while waiting for caller code. This cannot interrupt arbitrary JavaScript. */
|
|
26
|
+
public async wait<Result>(task: PromiseLike<Result>): Promise<Result> {
|
|
27
|
+
const aborted = Promise.withResolvers<never>();
|
|
28
|
+
const abort = () => aborted.reject(this.signal.reason);
|
|
29
|
+
this.signal.addEventListener('abort', abort, { once: true });
|
|
30
|
+
if (this.signal.aborted) abort();
|
|
31
|
+
try { return await Promise.race([task, aborted.promise]); }
|
|
32
|
+
finally { this.signal.removeEventListener('abort', abort); }
|
|
33
|
+
}
|
|
34
|
+
public async *iterate<Row>(rows: AsyncIterable<Row> | Iterable<Row>): AsyncGenerator<Row> {
|
|
35
|
+
const asyncRows = rows as AsyncIterable<Row>;
|
|
36
|
+
const iterator = asyncRows[Symbol.asyncIterator]?.() ?? (rows as Iterable<Row>)[Symbol.iterator]?.();
|
|
37
|
+
if (!iterator) throw new LosslessClientError('invalid_argument', 'Insert input must be iterable.');
|
|
38
|
+
let completed = false;
|
|
39
|
+
try {
|
|
40
|
+
while (true) {
|
|
41
|
+
this.check();
|
|
42
|
+
const result = await this.wait(Promise.resolve(iterator.next()));
|
|
43
|
+
if (result.done) { completed = true; return; }
|
|
44
|
+
this.check();
|
|
45
|
+
yield result.value;
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
// Request source cleanup even after cancellation. A caller-owned producer must cooperate
|
|
49
|
+
// with the supplied signal to stop its own pending I/O; no backend lease remains attached.
|
|
50
|
+
if (!completed && iterator.return) await this.wait(Promise.resolve(iterator.return()));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
public cancel(reason = new LosslessClientError('closed', 'Connection closed.')): void {
|
|
54
|
+
this.controller.abort(reason);
|
|
55
|
+
}
|
|
56
|
+
public dispose(): void {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.upstream?.removeEventListener('abort', this.onAbort);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TBackend = 'nosqldb' | 'mongodb' | 'sqldb' | 'mariadb' | 'clickhouse' | 'objectstorage' | 's3';
|
|
2
|
+
export type TCapability = 'available' | 'unsupported' | 'unknown';
|
|
3
|
+
|
|
4
|
+
export interface ICapabilities {
|
|
5
|
+
readonly backend: TBackend;
|
|
6
|
+
readonly transactions: TCapability;
|
|
7
|
+
readonly changeStreams: TCapability;
|
|
8
|
+
readonly objectMetrics: TCapability;
|
|
9
|
+
readonly diskResidentScans: TCapability;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IOperationOptions {
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IReadiness {
|
|
18
|
+
ready: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TClientErrorCode = 'invalid_argument' | 'unsupported_capability' | 'not_connected' |
|
|
23
|
+
'closed' | 'timeout' | 'cancelled' | 'result_limit' | 'authentication' | 'conflict' |
|
|
24
|
+
'backend_error' | 'ambiguous_write' | 'partial_write';
|
package/ts/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { LosslessOrgClient } from './classes.losslessorgclient.js';
|
|
2
|
+
export type { ILosslessOrgClientOptions } from './classes.losslessorgclient.js';
|
|
3
|
+
export { LosslessClientError } from './core/classes.error.js';
|
|
4
|
+
export type { ICapabilities, IOperationOptions, IReadiness, TBackend, TCapability, TClientErrorCode } from './core/interfaces.js';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { SmartdataCollection } from './classes.collection.js';
|
|
3
|
+
import {
|
|
4
|
+
runWithOrdinarySmartdataSession,
|
|
5
|
+
type TSmartdataOrdinarySession,
|
|
6
|
+
} from './classes.session.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Executes a model-validated atomic delete against MongoDB.
|
|
10
|
+
*
|
|
11
|
+
* This helper is package-internal. `SmartDataDbDoc.atomicDelete()` owns the
|
|
12
|
+
* public API and must validate every filter before calling this raw persistence
|
|
13
|
+
* boundary.
|
|
14
|
+
*/
|
|
15
|
+
export const executeAtomicDelete = async <T>(
|
|
16
|
+
collectionArg: SmartdataCollection<T>,
|
|
17
|
+
filterArg: plugins.mongodb.Filter<plugins.mongodb.Document>,
|
|
18
|
+
optsArg?: {
|
|
19
|
+
session?: TSmartdataOrdinarySession;
|
|
20
|
+
timeoutMS?: number;
|
|
21
|
+
},
|
|
22
|
+
): Promise<plugins.mongodb.DeleteResult> => {
|
|
23
|
+
return runWithOrdinarySmartdataSession(
|
|
24
|
+
optsArg?.session,
|
|
25
|
+
collectionArg.smartdataDb,
|
|
26
|
+
{
|
|
27
|
+
ordinaryWrite: true,
|
|
28
|
+
prepared: collectionArg.isInitializedForCurrentDatabase(),
|
|
29
|
+
preparationMessage:
|
|
30
|
+
`Initialize collection "${collectionArg.collectionName}" before using an owned SmartData session.`,
|
|
31
|
+
},
|
|
32
|
+
async (rawSessionArg) => {
|
|
33
|
+
await collectionArg.init();
|
|
34
|
+
return collectionArg.mongoDbCollection.deleteOne(
|
|
35
|
+
filterArg,
|
|
36
|
+
{
|
|
37
|
+
session: rawSessionArg,
|
|
38
|
+
timeoutMS: optsArg?.timeoutMS,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes a model-validated atomic delete-many against MongoDB.
|
|
47
|
+
*
|
|
48
|
+
* This helper is package-internal. `SmartDataDbDoc.atomicDeleteMany()` owns
|
|
49
|
+
* the public API and must validate every filter before calling this raw
|
|
50
|
+
* persistence boundary. Each matched document is deleted atomically; the
|
|
51
|
+
* batch as a whole is not isolated.
|
|
52
|
+
*/
|
|
53
|
+
export const executeAtomicDeleteMany = async <T>(
|
|
54
|
+
collectionArg: SmartdataCollection<T>,
|
|
55
|
+
filterArg: plugins.mongodb.Filter<plugins.mongodb.Document>,
|
|
56
|
+
optsArg?: {
|
|
57
|
+
session?: TSmartdataOrdinarySession;
|
|
58
|
+
timeoutMS?: number;
|
|
59
|
+
},
|
|
60
|
+
): Promise<plugins.mongodb.DeleteResult> => {
|
|
61
|
+
return runWithOrdinarySmartdataSession(
|
|
62
|
+
optsArg?.session,
|
|
63
|
+
collectionArg.smartdataDb,
|
|
64
|
+
{
|
|
65
|
+
ordinaryWrite: true,
|
|
66
|
+
prepared: collectionArg.isInitializedForCurrentDatabase(),
|
|
67
|
+
preparationMessage:
|
|
68
|
+
`Initialize collection "${collectionArg.collectionName}" before using an owned SmartData session.`,
|
|
69
|
+
},
|
|
70
|
+
async (rawSessionArg) => {
|
|
71
|
+
await collectionArg.init();
|
|
72
|
+
return collectionArg.mongoDbCollection.deleteMany(
|
|
73
|
+
filterArg,
|
|
74
|
+
{
|
|
75
|
+
session: rawSessionArg,
|
|
76
|
+
timeoutMS: optsArg?.timeoutMS,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { SmartdataCollection } from './classes.collection.js';
|
|
3
|
+
import {
|
|
4
|
+
normalizeOrdinaryPersistenceError,
|
|
5
|
+
SmartdataPersistenceError,
|
|
6
|
+
} from './classes.persistence.js';
|
|
7
|
+
import {
|
|
8
|
+
runWithOrdinarySmartdataSession,
|
|
9
|
+
isSmartdataSession,
|
|
10
|
+
failSmartdataTransaction,
|
|
11
|
+
type SmartdataSession,
|
|
12
|
+
type TSmartdataOrdinarySession,
|
|
13
|
+
} from './classes.session.js';
|
|
14
|
+
import { validateOrdinaryStoredDocument } from './classes.ordinarypersistence.js';
|
|
15
|
+
|
|
16
|
+
type TRawDocument = plugins.mongodb.WithId<plugins.mongodb.Document>;
|
|
17
|
+
|
|
18
|
+
export type TAtomicFindOneAndUpdateExecutionResult =
|
|
19
|
+
| { status: 'matched'; document: TRawDocument }
|
|
20
|
+
| { status: 'upserted'; document: TRawDocument | null }
|
|
21
|
+
| { status: 'not_matched'; document: null };
|
|
22
|
+
|
|
23
|
+
const unsupportedResult = (): never => {
|
|
24
|
+
throw new SmartdataPersistenceError(
|
|
25
|
+
'unsupported_operation',
|
|
26
|
+
'MongoDB returned unsupported atomic find-one-and-update metadata.',
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const classifyResult = (
|
|
31
|
+
resultArg: plugins.mongodb.ModifyResult<plugins.mongodb.Document>,
|
|
32
|
+
optionsArg: {
|
|
33
|
+
returnDocument: 'before' | 'after';
|
|
34
|
+
upsert: boolean;
|
|
35
|
+
},
|
|
36
|
+
): TAtomicFindOneAndUpdateExecutionResult => {
|
|
37
|
+
const metadata = resultArg.lastErrorObject;
|
|
38
|
+
if (resultArg.ok !== 1 || !metadata) return unsupportedResult();
|
|
39
|
+
const hasUpserted = Object.prototype.hasOwnProperty.call(metadata, 'upserted');
|
|
40
|
+
const upserted = metadata.upserted;
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
metadata.n === 1
|
|
44
|
+
&& metadata.updatedExisting === true
|
|
45
|
+
&& !hasUpserted
|
|
46
|
+
&& resultArg.value !== null
|
|
47
|
+
) {
|
|
48
|
+
return { status: 'matched', document: resultArg.value };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
optionsArg.upsert
|
|
53
|
+
&& metadata.n === 1
|
|
54
|
+
&& metadata.updatedExisting === false
|
|
55
|
+
&& hasUpserted
|
|
56
|
+
&& upserted !== undefined
|
|
57
|
+
&& upserted !== null
|
|
58
|
+
&& (
|
|
59
|
+
(optionsArg.returnDocument === 'before' && resultArg.value === null)
|
|
60
|
+
|| (optionsArg.returnDocument === 'after' && resultArg.value !== null)
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
63
|
+
return { status: 'upserted', document: resultArg.value };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
!optionsArg.upsert
|
|
68
|
+
&& metadata.n === 0
|
|
69
|
+
&& metadata.updatedExisting === false
|
|
70
|
+
&& !hasUpserted
|
|
71
|
+
&& resultArg.value === null
|
|
72
|
+
) {
|
|
73
|
+
return { status: 'not_matched', document: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return unsupportedResult();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Executes a model-validated atomic find-one-and-update against MongoDB.
|
|
81
|
+
*
|
|
82
|
+
* This helper is package-internal. It classifies driver metadata before
|
|
83
|
+
* returning and never exposes a MongoDB ModifyResult to the public model API.
|
|
84
|
+
*/
|
|
85
|
+
export const executeAtomicFindOneAndUpdate = async <T>(
|
|
86
|
+
collectionArg: SmartdataCollection<T>,
|
|
87
|
+
filterArg: plugins.mongodb.Filter<plugins.mongodb.Document>,
|
|
88
|
+
updateArg: plugins.mongodb.UpdateFilter<plugins.mongodb.Document>,
|
|
89
|
+
optionsArg: {
|
|
90
|
+
returnDocument: 'before' | 'after';
|
|
91
|
+
upsert: boolean;
|
|
92
|
+
sort?: Record<string, 1 | -1>;
|
|
93
|
+
session?: TSmartdataOrdinarySession;
|
|
94
|
+
},
|
|
95
|
+
): Promise<TAtomicFindOneAndUpdateExecutionResult> => {
|
|
96
|
+
const policy = collectionArg.getBoundModelSchema()?.ordinaryPersistence;
|
|
97
|
+
if (policy && (!isSmartdataSession(optionsArg.session) || optionsArg.returnDocument !== 'after')) {
|
|
98
|
+
throw new SmartdataPersistenceError('unsupported_operation',
|
|
99
|
+
'Validated ordinary updates require an owned transaction and an after image.');
|
|
100
|
+
}
|
|
101
|
+
return runWithOrdinarySmartdataSession(
|
|
102
|
+
optionsArg.session,
|
|
103
|
+
collectionArg.smartdataDb,
|
|
104
|
+
{
|
|
105
|
+
ordinaryWrite: true,
|
|
106
|
+
prepared: collectionArg.isInitializedForCurrentDatabase(),
|
|
107
|
+
preparationMessage:
|
|
108
|
+
`Initialize collection "${collectionArg.collectionName}" before using an owned SmartData session.`,
|
|
109
|
+
},
|
|
110
|
+
async (rawSessionArg) => {
|
|
111
|
+
if (policy && !rawSessionArg?.inTransaction()) {
|
|
112
|
+
throw new SmartdataPersistenceError('unsupported_operation',
|
|
113
|
+
'Validated ordinary updates require an active owned transaction.');
|
|
114
|
+
}
|
|
115
|
+
await collectionArg.init();
|
|
116
|
+
try {
|
|
117
|
+
const result = await collectionArg.mongoDbCollection.findOneAndUpdate(
|
|
118
|
+
filterArg,
|
|
119
|
+
updateArg,
|
|
120
|
+
{
|
|
121
|
+
includeResultMetadata: true,
|
|
122
|
+
returnDocument: optionsArg.returnDocument,
|
|
123
|
+
upsert: optionsArg.upsert,
|
|
124
|
+
...(optionsArg.sort ? { sort: optionsArg.sort } : {}),
|
|
125
|
+
session: rawSessionArg,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
try {
|
|
129
|
+
const classified = classifyResult(result, optionsArg);
|
|
130
|
+
if (policy && classified.document) {
|
|
131
|
+
classified.document = validateOrdinaryStoredDocument(
|
|
132
|
+
classified.document, policy, collectionArg.getBoundModelSchema()!.persistedFields,
|
|
133
|
+
) as TRawDocument;
|
|
134
|
+
}
|
|
135
|
+
return classified;
|
|
136
|
+
} catch (errorArg) {
|
|
137
|
+
if (policy) {
|
|
138
|
+
failSmartdataTransaction(
|
|
139
|
+
optionsArg.session as SmartdataSession, collectionArg.smartdataDb, errorArg,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
throw errorArg;
|
|
143
|
+
}
|
|
144
|
+
} catch (errorArg) {
|
|
145
|
+
return normalizeOrdinaryPersistenceError(
|
|
146
|
+
errorArg,
|
|
147
|
+
`Atomic find-one-and-update conflicts with a unique index in collection "${collectionArg.collectionName}".`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
};
|