UNPKG

node-cosmos

Version:

A light weight azure cosmosdb client aiming at ease of use for creating REST API. Supports json filter, sort and offset/limit

238 lines 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresDatabaseImpl = exports.ensureIdentifier = exports.addExpireAt = void 0; const CosmosDatabase_1 = require("../../CosmosDatabase"); const CosmosContainer_1 = require("../../CosmosContainer"); const PostgresConditionBuilder_1 = require("./PostgresConditionBuilder"); const assert_1 = require("../../../util/assert"); const uuid_1 = require("uuid"); /** Characters disallowed in schema/table names based on PostgreSQL identifier docs. */ const invalidIdentifierChars = [";", ",", "&", "'", "\\", "(", ")", "\t", "\n", "\r"]; /** Validates document ids for basic safety. */ const checkValidId = (id) => { if (!id) { throw new Error("id cannot be empty"); } if (id.includes("\t") || id.includes("\n") || id.includes("\r")) { throw new Error("id cannot contain \t or \n or \r"); } }; /** Adds a Cosmos-like timestamp stamp in seconds. */ const addTimestamp = (data) => { const epochMillis = Date.now(); data["_ts"] = epochMillis / 1000; }; /** Derives an `_expireAt` epoch second value when ttl is a valid number. */ const addExpireAt = (data) => { const ttlValue = data["ttl"]; if (typeof ttlValue !== "number" || !Number.isFinite(ttlValue)) { delete data["_expireAt"]; return undefined; } const ttlSeconds = Math.trunc(ttlValue); const epochSeconds = Math.floor(Date.now() / 1000) + ttlSeconds; data["_expireAt"] = epochSeconds; return epochSeconds; }; exports.addExpireAt = addExpireAt; /** Throws when the provided identifier is empty or invalid. */ const ensureIdentifier = (value, label) => { (0, assert_1.assertNotEmpty)(value, label); if (invalidIdentifierChars.some((ch) => value.includes(ch))) { throw new Error(`${label} should not contain invalid characters: ${value}`); } if (value.includes("--")) { throw new Error(`${label} should not contain '--': ${value}`); } }; exports.ensureIdentifier = ensureIdentifier; /** Wraps schema/table names with quotes and dot. */ const qualify = (schema, table) => { return `"${schema}"."${table}"`; }; /** * CosmosDatabase implementation that stores JSON documents inside * PostgreSQL tables and partitions them by schema/table mapping. */ class PostgresDatabaseImpl { /** * @param pool pg Pool used for queries. * @param dbName Logical database name, used to scope containers. */ constructor(pool, dbName) { /** Cached CosmosContainers keyed by logical collection name. */ this.collectionMap = new Map(); this.pool = pool; this.dbName = dbName; } /** Lazily creates a CosmosContainer for a collection. */ async createCollection(coll) { (0, exports.ensureIdentifier)(coll, "coll"); const container = new CosmosContainer_1.CosmosContainer(coll, { schema: coll }); this.collectionMap.set(coll, container); return container; } /** Removes cached metadata for a collection. */ async deleteCollection(coll) { this.collectionMap.delete(coll); } /** Gets or creates the CosmosContainer for the provided collection. */ async getCollection(coll) { let collection = this.collectionMap.get(coll); if (!collection) { collection = await this.createCollection(coll); } return collection; } /** Inserts a new document into `<schema>.<partition>` jsonb table. */ async create(coll, data, partition = coll) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, assert_1.assertIsDefined)(data, "data"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const schema = coll; const table = partition; const fqtn = qualify(schema, table); const id = data.id || (0, uuid_1.v4)().toString(); checkValidId(id); const payload = { ...data, id }; payload[CosmosDatabase_1._partition] = partition; addTimestamp(payload); (0, exports.addExpireAt)(payload); const text = `INSERT INTO ${fqtn} (id, data) VALUES ($1, $2::jsonb) RETURNING data`; const values = [id, JSON.stringify(payload)]; const result = await this.pool.query(text, values); return this.extractResource(result); } /** Reads a document and throws 404 when not found. */ async read(coll, id, partition = coll) { const resource = await this.readOrDefault(coll, id, partition, null); if (!resource) { throw new CosmosDatabase_1.CosmosError(undefined, 404, `item not found. id:${id}`); } return resource; } /** Reads a document and falls back to default when missing. */ async readOrDefault(coll, id, partition, defaultValue) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, assert_1.assertNotEmpty)(id, "id"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const fqtn = qualify(coll, partition); const text = `SELECT data FROM ${fqtn} WHERE id = $1`; const values = [id]; const result = await this.pool.query(text, values); if (!result.rowCount) { return defaultValue; } return result.rows[0].data; } /** Inserts or replaces a document by id. */ async upsert(coll, data, partition = coll) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, assert_1.assertIsDefined)(data, "data"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const schema = coll; const table = partition; const fqtn = qualify(schema, table); const id = data.id || (0, uuid_1.v4)().toString(); checkValidId(id); const payload = { ...data, id }; payload[CosmosDatabase_1._partition] = partition; addTimestamp(payload); (0, exports.addExpireAt)(payload); const text = `INSERT INTO ${fqtn} (id, data) VALUES ($1, $2::jsonb) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data RETURNING data`; const values = [id, JSON.stringify(payload)]; const result = await this.pool.query(text, values); return this.extractResource(result); } /** Updates an existing document and merges payload with stored value. */ async update(coll, data, partition = coll) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, assert_1.assertIsDefined)(data, "data"); (0, assert_1.assertIsDefined)(data.id, "data.id"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const existing = await this.read(coll, data.id, partition); const payload = { ...existing, ...data }; payload[CosmosDatabase_1._partition] = partition; addTimestamp(payload); (0, exports.addExpireAt)(payload); const fqtn = qualify(coll, partition); const text = `UPDATE ${fqtn} SET data = $1::jsonb WHERE id = $2 RETURNING data`; const targetId = payload.id; const values = [JSON.stringify(payload), targetId]; const result = await this.pool.query(text, values); if (!result.rowCount) { throw new CosmosDatabase_1.CosmosError(undefined, 404, `item not found. id:${data.id}`); } return this.extractResource(result); } /** Deletes a document by id and returns the CosmosId when successful. */ async delete(coll, id, partition = coll) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, assert_1.assertNotEmpty)(id, "id"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const fqtn = qualify(coll, partition); const text = `DELETE FROM ${fqtn} WHERE id = $1 RETURNING id`; const values = [id]; const result = await this.pool.query(text, values); if (!result.rowCount) { return undefined; } return { id }; } /** Finds documents by translating a Condition into SQL. */ async find(coll, condition, partition = coll) { (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const fqtn = qualify(coll, partition); const builder = new PostgresConditionBuilder_1.PostgresConditionBuilder(); const { whereClause, orderClause, limitClause, params } = builder.build(condition); const clauses = [whereClause, orderClause, limitClause].filter((clause) => clause); const text = [`SELECT data FROM ${fqtn} AS t`, ...clauses].join(" "); const result = await this.pool.query(text, params); return result.rows.map((row) => row.data); } /** Not implemented helper, kept for Cosmos interface compatibility. */ async findBySQL(coll, query, partition) { throw new Error("findBySQL is not supported for postgresql"); } /** Counts documents matching the provided Condition. */ async count(coll, condition, partition = coll) { var _a; (0, assert_1.assertNotEmpty)(coll, "coll"); (0, assert_1.assertNotEmpty)(partition, "partition"); (0, exports.ensureIdentifier)(coll, "coll"); (0, exports.ensureIdentifier)(partition, "partition"); const fqtn = qualify(coll, partition); const builder = new PostgresConditionBuilder_1.PostgresConditionBuilder(); const { whereClause, params } = builder.build(condition, true); const clauses = [whereClause].filter((clause) => clause); const text = [`SELECT COUNT(*)::int AS count FROM ${fqtn} AS t`, ...clauses].join(" "); const result = await this.pool.query(text, params); return Number(((_a = result.rows[0]) === null || _a === void 0 ? void 0 : _a.count) || 0); } /** Extracts the first row data or throws CosmosError when empty. */ extractResource(result) { (0, assert_1.assertIsDefined)(result.rowCount); if (!result.rowCount) { throw new CosmosDatabase_1.CosmosError(undefined, 404, "item not found"); } return result.rows[0].data; } } exports.PostgresDatabaseImpl = PostgresDatabaseImpl; //# sourceMappingURL=PostgresDatabaseImpl.js.map