@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
118 lines • 4.65 kB
JavaScript
;
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