UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

118 lines 4.65 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgTimelineStorage = void 0; const defaults_1 = __importDefault(require("lodash/defaults")); const first_1 = __importDefault(require("lodash/first")); const sortBy_1 = __importDefault(require("lodash/sortBy")); const Shard_1 = require("../abstract/Shard"); const ShardError_1 = require("../abstract/ShardError"); const TimelineStorage_1 = require("../ent/TimelineStorage"); const misc_1 = require("../internal/misc"); const escapeIdent_1 = require("./helpers/escapeIdent"); /** * An append-only (with compaction) timeline storage for PG. The timelines are * always appended to the table, but from time to time, when the number of * chunks per principal exceeds the limit, the timelines are read back, * compacted and written back as a single row. This is race condition safe, * since timelines merging is an associative and idempotent operation, i.e. * (T1+T2)+T3 == T1+(T2+T3); in the worst case, we'll just have slightly * suboptimal timeline rows. * * The expected table schema is: * ``` * CREATE UNLOGGED TABLE timelines( * id bigserial PRIMARY KEY, * principal text NOT NULL, * data text NOT NULL, * created_at timestamptz NOT NULL * ); * CREATE INDEX timelines_principal ON timelines (principal); * ``` * * Notes: * 1. Index on `principal` must be non-unique, since there may be multiple * records with the same value. * 2. The `id` field should have sequential auto-increment, since it's used for * garbage collection. * 3. The table must exist in all microshards (including global shard). */ class PgTimelineStorage extends (_b = TimelineStorage_1.TimelineStorage) { /** * Initializes an instance of PgTimelineStorage. */ constructor(options) { super(options); this.options = (0, defaults_1.default)({}, options, this.options, _a.DEFAULT_OPTIONS); } async load(principal) { const rows = await this.query(principal, "TIMELINES_SELECT", ["SELECT data FROM %T WHERE principal=?", principal]); return (0, sortBy_1.default)(rows, (row) => row.id).map((row) => row.data); } async save(principal, dataStr) { const row = (0, first_1.default)(await this.query(principal, "TIMELINES_INSERT", [ "INSERT INTO %T (principal, data, created_at) VALUES (?, ?, now())\n" + "RETURNING id, (SELECT array_agg(id||':'||data) FROM %T WHERE principal=?) AS chunks", principal, dataStr, principal, ])); if (!row.chunks || row.chunks.length <= (0, misc_1.maybeCall)(this.options.maxChunksPerPrincipal) - 1) { return; } let dataStrs = [[String(row.id), dataStr]]; for (const chunk of row.chunks) { const pos = chunk.indexOf(":"); dataStrs.push([chunk.substring(0, pos), chunk.substring(pos + 1)]); } dataStrs = (0, sortBy_1.default)(dataStrs, ([id, _]) => id); dataStr = this.options.merge(dataStrs.map(([_, data]) => data)); await this.query(principal, "TIMELINES_INSERT", [ "INSERT INTO %T (principal, data, created_at) VALUES (?, ?, now())", principal, dataStr, ]); await this.query(principal, "TIMELINES_DELETE", [ "DELETE FROM %T WHERE id=ANY(?)", dataStrs.map(([id]) => id), ]); } async query(principal, op, query) { let shard; try { shard = this.options.cluster.shard(principal); } catch (e) { if (e instanceof ShardError_1.ShardError) { shard = this.options.cluster.globalShard(); } else { throw e; } } const client = await shard.client(Shard_1.MASTER); return client.query({ query: [ String(query[0]).replace(/%T/g, (0, escapeIdent_1.escapeIdent)(this.options.table)), ...query.slice(1), ], isWrite: true, annotations: [], op, table: this.options.table, }); } } exports.PgTimelineStorage = PgTimelineStorage; _a = PgTimelineStorage; /** Default values for the constructor options. */ PgTimelineStorage.DEFAULT_OPTIONS = { ...Reflect.get(_b, "DEFAULT_OPTIONS", _a), table: "timelines", maxChunksPerPrincipal: 10, }; //# sourceMappingURL=PgTimelineStorage.js.map