UNPKG

@clickup/ent-framework

Version:

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

233 lines 10.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShardLocator = void 0; const uniq_1 = __importDefault(require("lodash/uniq")); const ShardError_1 = require("../abstract/ShardError"); const misc_1 = require("../internal/misc"); const types_1 = require("../types"); const EntNotFoundError_1 = require("./errors/EntNotFoundError"); const ShardAffinity_1 = require("./ShardAffinity"); const VC_1 = require("./VC"); /** * Knows how to locate Shard(s) based on various inputs. In some contexts, we * expect exactly one Shard returned, and in other contexts, multiple Shards are * okay. */ class ShardLocator { constructor({ cluster, entName, shardAffinity, uniqueKey, inverses, }) { this.cluster = cluster; this.entName = entName; this.shardAffinity = shardAffinity; this.idAndShardAffinity = [ types_1.ID, ...(this.shardAffinity instanceof Array ? this.shardAffinity : []), ]; this.uniqueKey = uniqueKey; this.inverses = inverses; this.globalShard = cluster.globalShard(); } /** * Called in a context when we must know exactly 1 Shard to work with (e.g. * INSERT, UPSERT etc.). If op === "insert" (fallback to random Shard), then * returns a random Shard in case when it can't infer the Shard number from * the input (used in e.g. INSERT operations); otherwise throws ShardError * (happens in e.g. UPSERT). * * The "randomness" of the "random Shard" is deterministic by the Ent's unique * key (if it's defined), so Ents with the same unique key will map to the * same "random" Shard (considering the total number of discovered Shards is * unchanged). Notice that this logic applies at INSERT time: since we often * times add Shards to the Cluster, we can't rely on it consistently at SELECT * time (but relying at INSERT time is more or less fine: it protects against * most of "unique key violation" problems, although still doesn't prevent all * of them for a fraction of the second when the number of Shards has just * been changed). */ async singleShardForInsert(input, op) { let shard = await this.singleShardFromAffinity(input, op); if (!shard && op === "insert") { shard = await this.cluster.randomShard(this.uniqueKey?.length ? this.uniqueKey.map((field) => input[field]) : undefined); } if (!shard) { throw new ShardError_1.ShardError(this.buildShardErrorMessage({ op, fields: this.shardAffinity instanceof Array ? this.shardAffinity : [types_1.ID], input, })); } return shard; } /** * Called in a context when multiple Shards may be involved, e.g. when * selecting Ents referred by some Inverses. May also return the empty list of * Shards when, although there are fields with Inverses in input (i.e. the * filtering is correct), there are no Inverse rows existing in the database. */ async multiShardsFromInput(vc, input, op) { const singleShard = await this.singleShardFromAffinity(input, op); if (singleShard) { return [singleShard]; } // Scan Inverses from left to right (assuming the leftmost Inverses are // lower in cardinality) and check whether our input has a filtering field // defined for that Inverse. If so, locate Shards based on that 1st found // field only (because it makes no sense to move to the next Inverse if we // can use a previous Inverse already). let hadInputFieldWithInverse = false; const shards = new Set(); for (const inverse of this.inverses) { const field = inverse.id2Field; const id1 = input[field]; if (id1 !== undefined) { hadInputFieldWithInverse = true; await (0, misc_1.mapJoin)(id1 instanceof Array ? id1 : [id1], async (id1) => { let id2s; try { id2s = await inverse.id2s(vc, id1); } catch (e) { throw e instanceof ShardError_1.ShardError ? new EntNotFoundError_1.EntNotFoundError(this.entName, { [field]: id1 }, e) : e; } for (const id2 of id2s) { const shard = await this.singleShardFromID(field, id2, op); if (shard) { shards.add(shard); } } }); break; } } if (!hadInputFieldWithInverse) { const inverseFields = this.inverses.map(({ id2Field }) => id2Field); throw new ShardError_1.ShardError(this.buildShardErrorMessage({ op, fields: (0, uniq_1.default)([ ...(this.shardAffinity instanceof Array ? this.shardAffinity : [types_1.ID]), ...inverseFields, ]), input, })); } return [...shards]; } /** * A wrapper for Cluster#shard() which injects Ent name to the exception (in * case of e.g. "Cannot locate Shard" exception). This is just a convenience * for debugging. * * If this method returns null, that means the caller should give up trying to * load the Ent with this ID, because it won't find it anyways (e.g. when we * try to load a sharded Ent using an ID from the global Shard). This is * identical to the case of an Ent not existing in the database. */ async singleShardFromID(field, id, op) { try { let shard; // GLOBAL_SHARD has precedence over a Shard number from ID (or any other // fields, since global tables may refer to global tables only). This allows // to move some previously sharded objects to the global Shard while doing // some refactoring. if (this.shardAffinity === ShardAffinity_1.GLOBAL_SHARD) { shard = this.globalShard; } else if (id === VC_1.GUEST_ID) { throw new ShardError_1.ShardError(this.buildShardErrorMessage({ op, why: "most likely you're trying to use a guest VC's principal instead of an ID", })); } else { if (id === null || id === undefined) { throw new ShardError_1.ShardError(this.buildShardErrorMessage({ op, why: `you should not pass null or undefined value in "${field}" field`, })); } shard = this.cluster.shard(id); if (shard.no === this.globalShard.no) { // We're trying to load a sharded Ent using an ID from the global // Shard. We know for sure that there will be no such Ent there then. return null; } } // We want to throw ShardError early to wrap the possible exception with // EntNotFoundError below. This is a little kludge, since on success, it // will call into Shard#options.locateClient() twice (here and when // running the actual query). Also, assertDiscoverable() is used only in // this single place. await shard.assertDiscoverable(); return shard; } catch (e) { throw e instanceof ShardError_1.ShardError ? new EntNotFoundError_1.EntNotFoundError(this.entName, { [field]: id }, e) : e; } } /** * All shards for this particular Ent depending on its affinity. */ async allShards() { return this.shardAffinity === ShardAffinity_1.GLOBAL_SHARD ? [this.globalShard] : this.cluster.nonGlobalShards(); } /** * Infers Shard number from shardAffinity info and the input record. * - Returns null if it can't do this; the caller should likely throw in this * case (although not always). * - If a field's value is an array of IDs from multiple Shards, then returns * the 1st inferred Shard still. This is the current limitation: we don't * even try to infer multiple Shards if we have some affinity fields in the * request, which e.g. simplifies Ent creation logic. Cross-Shard logic is * only enabled when using Inverses; see multiShardsFromInput(). */ async singleShardFromAffinity(input, op) { // For a low number of a very global objects only. ATTENTION: GLOBAL_SHARD // has precedence over a Shard number from ID! This allows to move some // previously sharded objects to the global Shard while doing some // refactoring. if (this.shardAffinity === ShardAffinity_1.GLOBAL_SHARD) { return this.globalShard; } // Explicit info about which Shard to use. if (input["$shardOfID"] !== undefined) { return this.singleShardFromID("$shardOfID", input["$shardOfID"]?.toString(), op); } // An explicit list of fields is passed in SHARD_AFFINITY. for (const fromField of this.idAndShardAffinity) { const fromValue = input[fromField]; const value = fromValue instanceof Array ? fromValue[0] : fromValue; if (typeof value === "string" && value) { return this.singleShardFromID(fromField, value, op); } } // Couldn't detect Shard number from any of the sources. return null; } /** * A helper to build uniform ShardError error messages. */ buildShardErrorMessage({ op, why, fields, input, }) { throw new ShardError_1.ShardError(`${this.entName}: cannot detect shard in "${op}" query: ` + (typeof why === "string" ? why : (fields.length > 1 ? `at least one of non-empty "${fields.join(", ")}" fields` : `non-empty "${fields[0]}" field`) + " must be present at TOP LEVEL of the input, but got " + (0, misc_1.inspectCompact)(input))); } } exports.ShardLocator = ShardLocator; //# sourceMappingURL=ShardLocator.js.map