ts-prorm-orm 1.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/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* etcd store implementation.
|
|
4
|
+
*
|
|
5
|
+
* etcd is a distributed, strongly-consistent key-value store used for
|
|
6
|
+
* service discovery, distributed configuration and coordination primitives
|
|
7
|
+
* (leader election, distributed locking). It has no query language, no
|
|
8
|
+
* schema, and no `JOIN`/transaction-log concept in the SQL sense, so it does
|
|
9
|
+
* not fit the SQL-shaped `Dialect` interface used by the SQL dialects in
|
|
10
|
+
* this repo. Instead it implements the minimal `NoSqlStore` marker
|
|
11
|
+
* interface (`src/nosql/store.ts`) and exposes etcd's real primitives -
|
|
12
|
+
* key/value get/put/delete (including prefix range operations), watch
|
|
13
|
+
* (etcd's signature "subscribe to key/prefix changes" feature), leases
|
|
14
|
+
* (TTL-based key expiry with keep-alive), distributed locking, and
|
|
15
|
+
* compare-and-swap transactions - as a typed, promise-based API instead of
|
|
16
|
+
* forcing everything into a `query(sql)` shape.
|
|
17
|
+
*
|
|
18
|
+
* Uses the official `etcd3` driver.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.EtcdStore = void 0;
|
|
22
|
+
const etcd3_1 = require("etcd3");
|
|
23
|
+
const errors_1 = require("../../errors");
|
|
24
|
+
function wrapConnectionError(err, hosts) {
|
|
25
|
+
const parent = err instanceof Error ? err : new Error(String(err));
|
|
26
|
+
const host = Array.isArray(hosts) ? hosts.join(',') : hosts;
|
|
27
|
+
const message = /unavailable|econnrefused/i.test(parent.message)
|
|
28
|
+
? 'Connection refused'
|
|
29
|
+
: `Unable to connect to etcd: ${parent.message}`;
|
|
30
|
+
return new errors_1.ConnectionError(message, { parent, database: 'etcd', host });
|
|
31
|
+
}
|
|
32
|
+
function wrapDatabaseError(err, message) {
|
|
33
|
+
const parent = err instanceof Error ? err : new Error(String(err));
|
|
34
|
+
// Keep the `action` context (e.g. "etcd PUT failed") but don't discard the
|
|
35
|
+
// real underlying etcd error text, the same way the Redis store does.
|
|
36
|
+
const combined = message ? `${message}: ${parent.message}` : parent.message;
|
|
37
|
+
return errors_1.DatabaseError.from(parent, { message: combined });
|
|
38
|
+
}
|
|
39
|
+
function kvToRecord(kvs) {
|
|
40
|
+
return { ...kvs };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `EtcdStore` wraps an `etcd3` client and exposes etcd's real primitives:
|
|
44
|
+
* key/value get/put/delete (single-key and prefix-range), watch, leases,
|
|
45
|
+
* distributed locking, and compare-and-swap transactions.
|
|
46
|
+
*/
|
|
47
|
+
class EtcdStore {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.name = 'etcd';
|
|
50
|
+
this.library = 'etcd3';
|
|
51
|
+
this.client = null;
|
|
52
|
+
this.connected = false;
|
|
53
|
+
/** Leases granted through `grantLease()`, keyed by lease ID, so later calls can look them back up. */
|
|
54
|
+
this.leases = new Map();
|
|
55
|
+
this.options = options;
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
// Connection lifecycle
|
|
59
|
+
// ---------------------------------------------------------------------
|
|
60
|
+
toClientOptions() {
|
|
61
|
+
// Spread (rather than pick known fields one-by-one) so any additional
|
|
62
|
+
// etcd3 `IOptions` fields a caller passes through (e.g. `grpcOptions`,
|
|
63
|
+
// `faultHandling`) reach the underlying client too.
|
|
64
|
+
return { ...this.options };
|
|
65
|
+
}
|
|
66
|
+
async connect() {
|
|
67
|
+
if (this.connected && this.client) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const client = new etcd3_1.Etcd3(this.toClientOptions());
|
|
72
|
+
// etcd3's constructor doesn't itself dial out; issue a cheap call to
|
|
73
|
+
// surface connectivity problems (bad hosts, refused connections,
|
|
74
|
+
// auth failures) immediately, the same way RedisStore eagerly
|
|
75
|
+
// connects instead of waiting for the first real command.
|
|
76
|
+
await client.get('__prorm_connect_probe__').string();
|
|
77
|
+
this.client = client;
|
|
78
|
+
this.connected = true;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
this.client?.close();
|
|
82
|
+
this.client = null;
|
|
83
|
+
this.connected = false;
|
|
84
|
+
throw wrapConnectionError(err, this.options.hosts);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async disconnect() {
|
|
88
|
+
for (const lease of this.leases.values()) {
|
|
89
|
+
lease.release();
|
|
90
|
+
}
|
|
91
|
+
this.leases.clear();
|
|
92
|
+
if (this.client) {
|
|
93
|
+
this.client.close();
|
|
94
|
+
this.client = null;
|
|
95
|
+
}
|
|
96
|
+
this.connected = false;
|
|
97
|
+
}
|
|
98
|
+
isConnected() {
|
|
99
|
+
return this.connected && this.client !== null;
|
|
100
|
+
}
|
|
101
|
+
/** Returns the underlying `etcd3` client for anything not wrapped here. */
|
|
102
|
+
getClient() {
|
|
103
|
+
return this.requireClient();
|
|
104
|
+
}
|
|
105
|
+
requireClient() {
|
|
106
|
+
if (!this.client) {
|
|
107
|
+
throw new errors_1.ConnectionError('Not connected to etcd', { database: 'etcd' });
|
|
108
|
+
}
|
|
109
|
+
return this.client;
|
|
110
|
+
}
|
|
111
|
+
async exec(fn, action) {
|
|
112
|
+
const client = this.requireClient();
|
|
113
|
+
try {
|
|
114
|
+
return await fn(client);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw wrapDatabaseError(err, `etcd ${action} failed`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------
|
|
121
|
+
// JSON helpers (opt-in convenience on top of the raw string API)
|
|
122
|
+
// ---------------------------------------------------------------------
|
|
123
|
+
async putJSON(key, value, options = {}) {
|
|
124
|
+
return this.put(key, JSON.stringify(value), options);
|
|
125
|
+
}
|
|
126
|
+
async getJSON(key) {
|
|
127
|
+
const raw = await this.get(key);
|
|
128
|
+
if (raw === null) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
throw wrapDatabaseError(err, `etcd GETJSON failed (invalid JSON for key "${key}")`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------
|
|
139
|
+
// Single-key operations
|
|
140
|
+
// ---------------------------------------------------------------------
|
|
141
|
+
async get(key) {
|
|
142
|
+
return this.exec((c) => c.get(key).string(), 'GET');
|
|
143
|
+
}
|
|
144
|
+
async getBuffer(key) {
|
|
145
|
+
return this.exec((c) => c.get(key).buffer(), 'GET');
|
|
146
|
+
}
|
|
147
|
+
async put(key, value, options = {}) {
|
|
148
|
+
await this.exec(async (c) => {
|
|
149
|
+
const builder = c.put(key).value(value);
|
|
150
|
+
if (options.lease !== undefined) {
|
|
151
|
+
builder.lease(options.lease);
|
|
152
|
+
}
|
|
153
|
+
await builder.exec();
|
|
154
|
+
}, 'PUT');
|
|
155
|
+
}
|
|
156
|
+
/** Deletes a single key. Returns the number of keys actually deleted (`0` or `1`). */
|
|
157
|
+
async delete(key) {
|
|
158
|
+
return this.exec(async (c) => {
|
|
159
|
+
const res = await c.delete().key(key).exec();
|
|
160
|
+
return Number(res.deleted);
|
|
161
|
+
}, 'DELETE');
|
|
162
|
+
}
|
|
163
|
+
async exists(key) {
|
|
164
|
+
return this.exec((c) => c.get(key).exists(), 'EXISTS');
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------
|
|
167
|
+
// Prefix / range operations
|
|
168
|
+
// ---------------------------------------------------------------------
|
|
169
|
+
/** All key/value pairs whose key starts with `prefix`. */
|
|
170
|
+
async getPrefix(prefix) {
|
|
171
|
+
return this.exec(async (c) => kvToRecord(await c.getAll().prefix(prefix).strings()), 'GETPREFIX');
|
|
172
|
+
}
|
|
173
|
+
/** All keys (without values) starting with `prefix`. */
|
|
174
|
+
async getKeysWithPrefix(prefix) {
|
|
175
|
+
return this.exec((c) => c.getAll().prefix(prefix).keys(), 'GETPREFIX KEYS');
|
|
176
|
+
}
|
|
177
|
+
/** Number of keys starting with `prefix`, without transferring their values. */
|
|
178
|
+
async countPrefix(prefix) {
|
|
179
|
+
return this.exec((c) => c.getAll().prefix(prefix).count(), 'COUNTPREFIX');
|
|
180
|
+
}
|
|
181
|
+
/** Deletes every key starting with `prefix`. Returns the number of keys deleted. */
|
|
182
|
+
async deletePrefix(prefix) {
|
|
183
|
+
return this.exec(async (c) => {
|
|
184
|
+
const res = await c.delete().prefix(prefix).exec();
|
|
185
|
+
return Number(res.deleted);
|
|
186
|
+
}, 'DELETEPREFIX');
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------
|
|
189
|
+
// Watch (etcd's signature feature: subscribe to key/prefix changes)
|
|
190
|
+
// ---------------------------------------------------------------------
|
|
191
|
+
attachWatchHandler(watcher, handler) {
|
|
192
|
+
watcher.on('put', (kv, previous) => {
|
|
193
|
+
handler({
|
|
194
|
+
type: 'put',
|
|
195
|
+
key: kv.key.toString(),
|
|
196
|
+
value: kv.value.toString(),
|
|
197
|
+
previousValue: previous ? previous.value.toString() : null,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
watcher.on('delete', (kv, previous) => {
|
|
201
|
+
handler({
|
|
202
|
+
type: 'delete',
|
|
203
|
+
key: kv.key.toString(),
|
|
204
|
+
value: null,
|
|
205
|
+
previousValue: previous ? previous.value.toString() : null,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Watch a single key for changes, invoking `handler` for every `put`/
|
|
211
|
+
* `delete` on that key. Returns a handle whose `cancel()` stops the
|
|
212
|
+
* underlying watch stream.
|
|
213
|
+
*/
|
|
214
|
+
async watch(key, handler) {
|
|
215
|
+
return this.exec(async (c) => {
|
|
216
|
+
const watcher = await c.watch().key(key).withPreviousKV().create();
|
|
217
|
+
this.attachWatchHandler(watcher, handler);
|
|
218
|
+
return { watcher, cancel: () => watcher.cancel() };
|
|
219
|
+
}, 'WATCH');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Watch every key starting with `prefix` for changes, invoking `handler`
|
|
223
|
+
* for every `put`/`delete` on any matching key. Returns a handle whose
|
|
224
|
+
* `cancel()` stops the underlying watch stream.
|
|
225
|
+
*/
|
|
226
|
+
async watchPrefix(prefix, handler) {
|
|
227
|
+
return this.exec(async (c) => {
|
|
228
|
+
const watcher = await c.watch().prefix(prefix).withPreviousKV().create();
|
|
229
|
+
this.attachWatchHandler(watcher, handler);
|
|
230
|
+
return { watcher, cancel: () => watcher.cancel() };
|
|
231
|
+
}, 'WATCH');
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------
|
|
234
|
+
// Leases (TTL-based key expiry + keep-alive)
|
|
235
|
+
// ---------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Grants a new lease with the given TTL (in seconds). By default the
|
|
238
|
+
* lease is kept alive automatically in the background (`autoKeepAlive`);
|
|
239
|
+
* pass `{ autoKeepAlive: false }` to manage keep-alives manually via
|
|
240
|
+
* `keepAliveOnce()`. Returns the lease ID, which can be passed to
|
|
241
|
+
* `put(key, value, { lease })` to associate keys with the lease so they
|
|
242
|
+
* expire automatically when it does.
|
|
243
|
+
*/
|
|
244
|
+
async grantLease(ttlSeconds, options = {}) {
|
|
245
|
+
return this.exec(async (c) => {
|
|
246
|
+
const leaseOptions = { autoKeepAlive: options.autoKeepAlive ?? true };
|
|
247
|
+
const lease = c.lease(ttlSeconds, leaseOptions);
|
|
248
|
+
const id = await lease.grant();
|
|
249
|
+
this.leases.set(id, lease);
|
|
250
|
+
return id;
|
|
251
|
+
}, 'LEASE GRANT');
|
|
252
|
+
}
|
|
253
|
+
/** Puts `key`/`value` under a previously granted lease, so it expires when the lease does. */
|
|
254
|
+
async putWithLease(key, value, leaseId) {
|
|
255
|
+
const lease = this.requireLease(leaseId);
|
|
256
|
+
try {
|
|
257
|
+
await lease.put(key).value(value).exec();
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
throw wrapDatabaseError(err, 'etcd PUT (with lease) failed');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Fires a single, immediate keep-alive for a lease, resetting its TTL countdown. */
|
|
264
|
+
async keepAliveOnce(leaseId) {
|
|
265
|
+
const lease = this.requireLease(leaseId);
|
|
266
|
+
try {
|
|
267
|
+
await lease.keepaliveOnce();
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
throw wrapDatabaseError(err, 'etcd LEASE KEEPALIVE failed');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Revokes a lease immediately, evicting every key still attached to it. */
|
|
274
|
+
async revokeLease(leaseId) {
|
|
275
|
+
const lease = this.requireLease(leaseId);
|
|
276
|
+
try {
|
|
277
|
+
await lease.revoke();
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
this.leases.delete(leaseId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Stops sending keep-alives for a lease, letting it expire naturally when
|
|
285
|
+
* its TTL elapses, instead of revoking it immediately. Use `revokeLease()`
|
|
286
|
+
* to evict its keys right away.
|
|
287
|
+
*/
|
|
288
|
+
releaseLease(leaseId) {
|
|
289
|
+
const lease = this.leases.get(leaseId);
|
|
290
|
+
lease?.release();
|
|
291
|
+
this.leases.delete(leaseId);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Registers a handler that fires when etcd indicates a lease has been
|
|
295
|
+
* lost (TTL expired without a successful keep-alive, or it was revoked
|
|
296
|
+
* server-side). Not fired when `revokeLease()`/`releaseLease()` is called
|
|
297
|
+
* locally and succeeds normally.
|
|
298
|
+
*/
|
|
299
|
+
onLeaseLost(leaseId, handler) {
|
|
300
|
+
const lease = this.requireLease(leaseId);
|
|
301
|
+
lease.on('lost', handler);
|
|
302
|
+
}
|
|
303
|
+
requireLease(leaseId) {
|
|
304
|
+
const lease = this.leases.get(leaseId);
|
|
305
|
+
if (!lease) {
|
|
306
|
+
throw new errors_1.DatabaseError(`Unknown etcd lease "${leaseId}" (not granted through this store instance)`);
|
|
307
|
+
}
|
|
308
|
+
return lease;
|
|
309
|
+
}
|
|
310
|
+
// ---------------------------------------------------------------------
|
|
311
|
+
// Distributed locking
|
|
312
|
+
// ---------------------------------------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* Acquires a distributed lock on `key`, blocking (queueing behind the
|
|
315
|
+
* current holder, in acquisition order) until it's free rather than
|
|
316
|
+
* rejecting immediately. Under the hood this is a lease on the key
|
|
317
|
+
* that's revoked on `release()`, or timed out by etcd if the holder
|
|
318
|
+
* dies - `ttlSeconds` controls that lease's TTL (etcd3 defaults to 30s).
|
|
319
|
+
*
|
|
320
|
+
* Note: `etcd3`'s own `Lock#acquire()` is a single compare-and-swap
|
|
321
|
+
* attempt - it *rejects immediately* with `EtcdLockFailedError` if the
|
|
322
|
+
* key already exists, rather than waiting, despite this store's docs
|
|
323
|
+
* (and the in-repo test mock) describing blocking/queueing semantics.
|
|
324
|
+
* To actually deliver that documented contract against a real etcd
|
|
325
|
+
* server, `acquireLockOrThrow()` below retries: on
|
|
326
|
+
* `EtcdLockFailedError` it watches the key until it's deleted (i.e. the
|
|
327
|
+
* current holder released or its lease expired), then retries the CAS.
|
|
328
|
+
* Multiple waiters can wake and race the retry simultaneously; that's
|
|
329
|
+
* fine because the retry is itself a CAS - at most one of them wins
|
|
330
|
+
* each round, and the rest go back to waiting.
|
|
331
|
+
*/
|
|
332
|
+
async acquireLock(key, ttlSeconds) {
|
|
333
|
+
return this.exec(async (c) => {
|
|
334
|
+
const lock = await this.acquireLockOrThrow(c, key, ttlSeconds);
|
|
335
|
+
return this.toLockHandle(lock);
|
|
336
|
+
}, 'LOCK');
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Wraps an acquired `etcd3` `Lock` in an `EtcdLockHandle`. `etcd3`'s own
|
|
340
|
+
* `Lock#leaseId()` keeps returning the (now-revoked) lease ID even after
|
|
341
|
+
* `release()` - verified against a real etcd server - which contradicts
|
|
342
|
+
* this handle's documented `null`-after-release contract, so track
|
|
343
|
+
* "released" locally instead of trusting the underlying lock for that.
|
|
344
|
+
*/
|
|
345
|
+
toLockHandle(lock) {
|
|
346
|
+
let released = false;
|
|
347
|
+
return {
|
|
348
|
+
leaseId: async () => (released ? null : lock.leaseId()),
|
|
349
|
+
release: async () => {
|
|
350
|
+
await lock.release();
|
|
351
|
+
released = true;
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Acquires a distributed lock on `key`, runs `fn`, and releases the lock
|
|
357
|
+
* once `fn`'s result settles (whether it resolves or rejects). This is
|
|
358
|
+
* the recommended way to use locks - it can't leak a held lock the way
|
|
359
|
+
* manual `acquireLock()`/`release()` pairing can if `fn` throws.
|
|
360
|
+
*/
|
|
361
|
+
async withLock(key, fn, ttlSeconds) {
|
|
362
|
+
const client = this.requireClient();
|
|
363
|
+
let lock;
|
|
364
|
+
try {
|
|
365
|
+
lock = await this.acquireLockOrThrow(client, key, ttlSeconds);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
throw wrapDatabaseError(err, 'etcd LOCK failed');
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const value = await fn();
|
|
372
|
+
await lock.release();
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
await lock.release().catch(() => undefined);
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Repeatedly attempts `client.lock(key).acquire()`, waiting for the key
|
|
382
|
+
* to be deleted (via a watch) and retrying whenever it fails with
|
|
383
|
+
* `EtcdLockFailedError`, so the returned promise doesn't settle until
|
|
384
|
+
* the lock is actually acquired (see `acquireLock()` above for why this
|
|
385
|
+
* is necessary against a real etcd server).
|
|
386
|
+
*/
|
|
387
|
+
async acquireLockOrThrow(client, key, ttlSeconds) {
|
|
388
|
+
for (;;) {
|
|
389
|
+
const lock = client.lock(key);
|
|
390
|
+
if (ttlSeconds !== undefined) {
|
|
391
|
+
lock.ttl(ttlSeconds);
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await lock.acquire();
|
|
395
|
+
return lock;
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
if (!(err instanceof etcd3_1.EtcdLockFailedError)) {
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
await this.waitForKeyDeletion(client, key);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** Resolves once `key` no longer exists (already gone, or deleted while we watch it). */
|
|
406
|
+
async waitForKeyDeletion(client, key) {
|
|
407
|
+
const stillThere = await client.get(key).exists();
|
|
408
|
+
if (!stillThere) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const watcher = await client.watch().key(key).create();
|
|
412
|
+
try {
|
|
413
|
+
await new Promise((resolve) => {
|
|
414
|
+
let settled = false;
|
|
415
|
+
const finish = () => {
|
|
416
|
+
if (settled) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
settled = true;
|
|
420
|
+
resolve();
|
|
421
|
+
};
|
|
422
|
+
watcher.on('delete', finish);
|
|
423
|
+
// The key may have been deleted between the exists() check above
|
|
424
|
+
// and the watcher attaching; re-check to avoid waiting forever.
|
|
425
|
+
client
|
|
426
|
+
.get(key)
|
|
427
|
+
.exists()
|
|
428
|
+
.then((exists) => {
|
|
429
|
+
if (!exists) {
|
|
430
|
+
finish();
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
.catch(() => finish());
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await watcher.cancel().catch(() => undefined);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------
|
|
441
|
+
// Transactions (compare-and-swap via etcd3's if()/then()/else())
|
|
442
|
+
// ---------------------------------------------------------------------
|
|
443
|
+
/**
|
|
444
|
+
* Starts a raw etcd transaction: `if (key.<column> <cmp> value) { ... }`.
|
|
445
|
+
* Returns etcd3's `ComparatorBuilder` directly so callers get its full
|
|
446
|
+
* `.and()/.then()/.else()/.commit()` chain; combine with `putOp()`/
|
|
447
|
+
* `deleteOp()`/`getOp()` below to build the `then`/`else` clauses.
|
|
448
|
+
*
|
|
449
|
+
* ```typescript
|
|
450
|
+
* const result = await store
|
|
451
|
+
* .ifCompare('config-version', 'Value', '==', '1')
|
|
452
|
+
* .then(store.putOp('config-version', '2'))
|
|
453
|
+
* .else(store.getOp('config-version'))
|
|
454
|
+
* .commit();
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
ifCompare(key, column, cmp, value) {
|
|
458
|
+
return this.requireClient().if(key, column, cmp, value);
|
|
459
|
+
}
|
|
460
|
+
/** A `put` operation builder, for use as a `.then()`/`.else()` clause in `ifCompare()`. */
|
|
461
|
+
putOp(key, value) {
|
|
462
|
+
return this.requireClient().put(key).value(value);
|
|
463
|
+
}
|
|
464
|
+
/** A `delete` operation builder, for use as a `.then()`/`.else()` clause in `ifCompare()`. */
|
|
465
|
+
deleteOp(key) {
|
|
466
|
+
return this.requireClient().delete().key(key);
|
|
467
|
+
}
|
|
468
|
+
/** A `get` operation builder, for use as a `.then()`/`.else()` clause in `ifCompare()`. */
|
|
469
|
+
getOp(key) {
|
|
470
|
+
return this.requireClient().get(key);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Convenience compare-and-swap: atomically sets `key` to `newValue` only
|
|
474
|
+
* if its current value equals `expectedValue` (or, if `expectedValue` is
|
|
475
|
+
* `null`, only if the key does not currently exist). Returns whether the
|
|
476
|
+
* swap happened.
|
|
477
|
+
*/
|
|
478
|
+
async compareAndSwap(key, expectedValue, newValue) {
|
|
479
|
+
return this.exec(async (c) => {
|
|
480
|
+
const comparator = expectedValue === null
|
|
481
|
+
? c.if(key, 'Create', '==', 0)
|
|
482
|
+
: c.if(key, 'Value', '==', expectedValue);
|
|
483
|
+
const result = await comparator.then(c.put(key).value(newValue)).commit();
|
|
484
|
+
return result.succeeded;
|
|
485
|
+
}, 'TXN');
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Atomically deletes `key` only if its current value equals
|
|
489
|
+
* `expectedValue`. Returns whether the delete happened.
|
|
490
|
+
*/
|
|
491
|
+
async compareAndDelete(key, expectedValue) {
|
|
492
|
+
return this.exec(async (c) => {
|
|
493
|
+
const result = await c
|
|
494
|
+
.if(key, 'Value', '==', expectedValue)
|
|
495
|
+
.then(c.delete().key(key))
|
|
496
|
+
.commit();
|
|
497
|
+
return result.succeeded;
|
|
498
|
+
}, 'TXN');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
exports.EtcdStore = EtcdStore;
|
|
502
|
+
exports.default = EtcdStore;
|