@dataplan/pg
Version:
PostgreSQL step classes for Grafast
790 lines • 33.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.PgResource = void 0;
exports.EXPORTABLE = EXPORTABLE;
exports.isForbidden = isForbidden;
exports.makeRegistry = makeRegistry;
exports.makeRegistryBuilder = makeRegistryBuilder;
exports.makePgResourceOptions = makePgResourceOptions;
const tslib_1 = require("tslib");
/* eslint-disable graphile-export/export-instances */
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const grafast_1 = require("grafast");
const pg_sql2_1 = tslib_1.__importDefault(require("pg-sql2"));
const codecs_js_1 = require("./codecs.js");
const inspect_js_1 = require("./inspect.js");
const pgSelect_js_1 = require("./steps/pgSelect.js");
function EXPORTABLE(factory, args, nameHint) {
const forbiddenIndex = args.findIndex(isForbidden);
if (forbiddenIndex >= 0) {
throw new Error(`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`);
}
const fn = factory(...args);
if (((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
!("$exporter$factory" in fn)) {
Object.defineProperties(fn, {
$exporter$args: { value: args },
$exporter$factory: { value: factory },
$exporter$name: { writable: true, value: nameHint },
});
}
return fn;
}
function isForbidden(thing) {
return ((typeof thing === "object" || typeof thing === "function") &&
thing !== null &&
"$$export" in thing &&
thing.$$export === false);
}
/**
* PgResource represents any resource of SELECT-able data in Postgres: tables,
* views, functions, etc.
*/
class PgResource {
/**
* @param from - the SQL for the `FROM` clause (without any
* aliasing). If this is a subquery don't forget to wrap it in parens.
* @param name - a nickname for this resource. Doesn't need to be unique
* (but should be). Used for making the SQL query and debug messages easier
* to understand.
*/
constructor(registry, options) {
// TODO: make a public interface for this information
/**
* If present, implies that the resource represents a `setof composite[]` (i.e.
* an array of arrays) - and thus is not appropriate to use for GraphQL
* Cursor Connections.
*
* @experimental
*/
this.sqlPartitionByIndex = null;
const { codec, executor, name, identifier, from, uniques, extensions, parameters, description, isUnique, sqlPartitionByIndex, isMutation, hasImplicitOrder, selectAuth, isList, isVirtual, } = options;
this.registry = registry;
this.extensions = extensions;
this.codec = codec;
this.executor = executor;
this.name = name;
this.identifier = identifier ?? name;
this.from = from;
this.uniques = uniques ?? [];
this.parameters = parameters;
this.description = description;
this.isUnique = !!isUnique;
this.sqlPartitionByIndex = sqlPartitionByIndex ?? null;
this.isMutation = !!isMutation;
this.hasImplicitOrder = hasImplicitOrder ?? false;
this.isList = !!isList;
this.isVirtual = isVirtual ?? false;
this.selectAuth = selectAuth;
// parameters is null iff from is not a function
const sourceIsFunction = typeof this.from === "function";
if (this.parameters == null && sourceIsFunction) {
throw new Error(`Resource ${this} is invalid - it's a function but without a parameters array. If the function accepts no parameters please pass an empty array.`);
}
if (this.parameters != null && !sourceIsFunction) {
throw new Error(`Resource ${this} is invalid - parameters can only be specified when the resource is a function.`);
}
if (this.codec.arrayOfCodec?.attributes) {
throw new Error(`Resource ${this} is invalid - creating a resource that returns an array of a composite type is forbidden; please \`unnest\` the array.`);
}
if (this.isUnique && this.sqlPartitionByIndex) {
throw new Error(`Resource ${this} is invalid - cannot be unique and also partitionable`);
}
}
/**
* Often you can access table records from a table directly but also from a
* view or materialized view. This method makes it convenient to construct
* multiple datasources that all represent the same underlying table
* type/relations/etc.
*/
static alternativeResourceOptions(baseOptions, overrideOptions) {
const { name, identifier, from, uniques, extensions } = overrideOptions;
const { codec, executor, selectAuth } = baseOptions;
return {
codec,
executor,
name,
identifier,
from,
uniques,
parameters: undefined,
extensions,
selectAuth,
};
}
/**
* Often you can access table records from a table directly but also from a
* number of functions. This method makes it convenient to construct multiple
* datasources that all represent the same underlying table
* type/relations/etc but pull their rows from functions.
*/
static functionResourceOptions(baseOptions, overrideOptions) {
const { codec, executor, selectAuth: originalSelectAuth } = baseOptions;
const { name, identifier, from: fnFrom, parameters, returnsSetof, returnsArray, uniques, extensions, isMutation, hasImplicitOrder, selectAuth: overrideSelectAuth, description, } = overrideOptions;
const selectAuth = overrideSelectAuth === null
? null
: (overrideSelectAuth ?? originalSelectAuth);
if (!returnsArray) {
// This is the easy case
return {
codec,
executor,
name,
identifier,
from: fnFrom,
uniques,
parameters,
extensions,
isUnique: !returnsSetof,
isMutation: Boolean(isMutation),
hasImplicitOrder,
selectAuth,
description,
};
}
else if (!returnsSetof) {
// This is a `composite[]` function; convert it to a `setof composite` function:
const from = EXPORTABLE((fnFrom, sql) => (...args) => sql `unnest(${fnFrom(...args)})`, [fnFrom, pg_sql2_1.default], `${name}_from`);
return {
codec,
executor,
name,
identifier,
from: from,
uniques,
parameters,
extensions,
isUnique: false, // set now, not unique
isMutation: Boolean(isMutation),
hasImplicitOrder,
selectAuth,
isList: true,
description,
};
}
else {
// This is a `setof composite[]` function; convert it to `setof composite` and indicate that we should partition it.
const sqlTmp = pg_sql2_1.default.identifier(Symbol(`${name}_tmp`));
const sqlPartitionByIndex = pg_sql2_1.default.identifier(Symbol(`${name}_idx`));
const from = EXPORTABLE((fnFrom, sql, sqlPartitionByIndex, sqlTmp) => (...args) => sql `${fnFrom(...args)} with ordinality as ${sqlTmp} (arr, ${sqlPartitionByIndex}) cross join lateral unnest (${sqlTmp}.arr)`, [fnFrom, pg_sql2_1.default, sqlPartitionByIndex, sqlTmp], `${name}_from`);
return {
codec,
executor,
name,
identifier,
from: from,
uniques,
parameters,
extensions,
isUnique: false, // set now, not unique
sqlPartitionByIndex,
isMutation: Boolean(isMutation),
hasImplicitOrder,
selectAuth,
description,
};
}
}
toString() {
return chalk_1.default.bold.blue(`PgResource(${this.name})`);
}
getRelations() {
return (this.registry.pgRelations[this.codec.name] ??
Object.create(null));
}
getRelation(name) {
return this.getRelations()[name];
}
resolveVia(via, attr) {
if (!via) {
throw new Error("No via to resolve");
}
const relationName = typeof via === "string" ? via : via.relation;
const attributeName = typeof via === "string" ? attr : via.attribute;
// Check
const relation = this.getRelation(relationName);
if (!relation) {
throw new Error(`Unknown relation '${relationName}' in ${this}`);
}
if (!relation.remoteResource.codec.attributes[attributeName]) {
throw new Error(`${this} relation '${relationName}' does not have attribute '${attributeName}'`);
}
const attribute = relation.remoteResource.codec.attributes[attributeName];
return {
relationName,
attributeName,
relation,
attribute,
};
}
// PERF: this needs optimization.
getReciprocal(otherCodec, otherRelationName) {
if (this.parameters) {
throw new Error(".getReciprocal() cannot be used with functional resources; please use .execute()");
}
const otherRelation = this.registry.pgRelations[otherCodec.name]?.[otherRelationName];
const relations = this.getRelations();
const reciprocal = Object.entries(relations).find(([_relationName, relation]) => {
if (relation.remoteResource.codec !== otherCodec) {
return false;
}
if (!(0, grafast_1.arraysMatch)(relation.localAttributes, otherRelation.remoteAttributes)) {
return false;
}
if (!(0, grafast_1.arraysMatch)(relation.remoteAttributes, otherRelation.localAttributes)) {
return false;
}
return true;
});
return reciprocal || null;
}
get(spec,
// This is internal, it's an optimisation we can use but you shouldn't.
_internalOptionsDoNotPass) {
if (this.parameters) {
throw new Error(".get() cannot be used with functional resources; please use .execute()");
}
if (!spec) {
throw new Error(`Cannot ${this}.get without a valid spec`);
}
const keys = Object.keys(spec);
if (!this.uniques.some((uniq) => uniq.attributes.every((key) => keys.includes(key)))) {
throw new Error(`Attempted to call ${this}.get({${keys.join(", ")}}) at child field (TODO: which one?) but that combination of attributes is not unique (uniques: ${JSON.stringify(this.uniques)}). Did you mean to call .find() instead?`);
}
return this.find(spec).single(_internalOptionsDoNotPass);
}
find(spec = Object.create(null)) {
if (this.parameters) {
throw new Error(".get() cannot be used with functional resources; please use .execute()");
}
if (!this.codec.attributes) {
throw new Error("Cannot call find if there's no attributes");
}
const attributes = this.codec.attributes;
const keys = Object.keys(spec); /* as Array<keyof typeof attributes>*/
const invalidKeys = keys.filter((key) => attributes[key] == null);
if (invalidKeys.length > 0) {
throw new Error(`Attempted to call ${this}.get({${keys.join(", ")}}) but that request included attributes that we don't know about: '${invalidKeys.join("', '")}'`);
}
const identifiers = keys.map((key) => {
const attribute = attributes[key];
if ("via" in attribute && attribute.via) {
throw new Error(`Attribute '${String(key)}' is defined with a 'via' and thus cannot be used as an identifier for '.find()' or '.get()' calls (requested keys: '${keys.join("', '")}').`);
}
const { codec } = attribute;
const stepOrConstant = spec[key];
if (stepOrConstant == undefined) {
throw new Error(`Attempted to call ${this}.find({${keys.join(", ")}}) but failed to provide a plan for '${String(key)}'`);
}
return {
step: stepOrConstant instanceof grafast_1.ExecutableStep
? stepOrConstant
: (0, grafast_1.constant)(stepOrConstant, false),
codec,
matches: (alias) => typeof attribute.expression === "function"
? attribute.expression(alias)
: (0, pg_sql2_1.default) `${alias}.${pg_sql2_1.default.identifier(key)}`,
};
});
return (0, pgSelect_js_1.pgSelect)({ resource: this, identifiers });
}
execute(args = [], mode = this.isMutation ? "mutation" : "normal") {
const $select = (0, pgSelect_js_1.pgSelect)({
resource: this,
identifiers: [],
args,
mode,
});
if (this.isUnique) {
return $select.single();
}
const sqlPartitionByIndex = this.sqlPartitionByIndex;
if (sqlPartitionByIndex) {
// We're a setof array of composite type function, e.g. `setof users[]`, so we need to reconstitute the plan.
return (0, grafast_1.partitionByIndex)($select, ($row) => $row.select(sqlPartitionByIndex, codecs_js_1.TYPES.int, true),
// Ordinality is 1-indexed but we want a 0-indexed number
1);
}
else {
return $select;
}
}
applyAuthorizationChecksToPlan($step) {
if (this.selectAuth) {
this.selectAuth($step);
}
// e.g. $step.where(sql`user_id = ${me}`);
return;
}
/**
* @deprecated Please use `.executor.context()` instead - all resources for the
* same executor must use the same context to allow for SQL inlining, unions,
* etc.
*/
context() {
return this.executor.context();
}
/** @internal */
executeWithCache(values, options) {
return this.executor.executeWithCache(values, options);
}
/** @internal */
executeWithoutCache(values, options) {
return this.executor.executeWithoutCache(values, options);
}
/** @internal */
executeStream(values, options) {
return this.executor.executeStream(values, options);
}
/** @internal */
executeMutation(options) {
return this.executor.executeMutation(options);
}
/**
* Returns an SQL fragment that evaluates to `'true'` (string) if the row is
* non-null and `'false'` or `null` otherwise.
*
* @see {@link PgCodec.notNullExpression}
*
* @internal
*/
getNullCheckExpression(alias) {
if (this.codec.notNullExpression) {
// Use the user-provided check
return this.codec.notNullExpression(alias);
}
else {
// Every column in a primary key is non-nullable; so just see if one is null
const pk = this.uniques.find((u) => u.isPrimary);
const nonNullableAttribute = (this.codec.attributes
? Object.entries(this.codec.attributes).find(([_attributeName, spec]) => !spec.via && !spec.expression && spec.notNull)?.[0]
: null) ?? pk?.attributes[0];
if (nonNullableAttribute) {
const firstAttribute = (0, pg_sql2_1.default) `${alias}.${pg_sql2_1.default.identifier(nonNullableAttribute)}`;
return (0, pg_sql2_1.default) `(not (${firstAttribute} is null))::text`;
}
else {
// Fallback
// NOTE: we cannot use `is distinct from null` here because it's
// commonly used for `select * from ((select my_table.composite).*)`
// and the rows there _are_ distinct from null even if the underlying
// data is not.
return (0, pg_sql2_1.default) `(not (${alias} is null))::text`;
}
}
}
}
exports.PgResource = PgResource;
(0, grafast_1.exportAs)("@dataplan/pg", PgResource, "PgResource");
function makeRegistry(config) {
const registry = {
pgExecutors: Object.create(null),
pgCodecs: Object.create(null),
pgResources: Object.create(null),
pgRelations: Object.create(null),
};
// Tell the system to read the built pgCodecs, pgResources, pgRelations from the registry
Object.defineProperties(registry.pgExecutors, {
$exporter$args: { value: [registry] },
$exporter$factory: {
value: (registry) => registry.pgExecutors,
},
});
Object.defineProperties(registry.pgCodecs, {
$exporter$args: { value: [registry] },
$exporter$factory: {
value: (registry) => registry.pgCodecs,
},
});
Object.defineProperties(registry.pgResources, {
$exporter$args: { value: [registry] },
$exporter$factory: {
value: (registry) => registry.pgResources,
},
});
Object.defineProperties(registry.pgRelations, {
$exporter$args: { value: [registry] },
$exporter$factory: {
value: (registry) => registry.pgRelations,
},
});
let addExecutorForbidden = false;
function addExecutor(executor) {
if (addExecutorForbidden) {
throw new Error(`It's too late to call addExecutor now`);
}
const executorName = executor.name;
if (registry.pgExecutors[executorName]) {
if (registry.pgExecutors[executorName] !== executor) {
console.dir({
existing: registry.pgExecutors[executorName],
new: executor,
});
throw new Error(`Executor named '${executorName}' is already registered; you cannot have two executors with the same name`);
}
return executor;
}
else {
// Custom spec, pin it back to the registry
registry.pgExecutors[executorName] = executor;
if (!executor.$$export && !executor.$exporter$factory) {
// Tell the system to read the built executor from the registry
Object.defineProperties(executor, {
$exporter$args: { value: [registry, executorName] },
$exporter$factory: {
value: (registry, executorName) => registry.pgExecutors[executorName],
},
});
}
return executor;
}
}
let addCodecForbidden = false;
function addCodec(codec) {
if (addCodecForbidden) {
throw new Error(`It's too late to call addCodec now`);
}
const codecName = codec.name;
if (registry.pgCodecs[codecName]) {
if (registry.pgCodecs[codecName] !== codec) {
console.dir({
existing: registry.pgCodecs[codecName],
new: codec,
});
throw new Error(`Codec named '${codecName}' is already registered; you cannot have two codecs with the same name`);
}
return codec;
}
else if (codec.$$export || codec.$exporter$factory) {
registry.pgCodecs[codecName] = codec;
return codec;
}
else {
// Custom spec, pin it back to the registry
registry.pgCodecs[codecName] = codec;
if (codec.attributes) {
const prevCols = codec.attributes;
for (const col of Object.values(prevCols)) {
addCodec(col.codec);
}
}
if (codec.arrayOfCodec) {
addCodec(codec.arrayOfCodec);
}
if (codec.domainOfCodec) {
addCodec(codec.domainOfCodec);
}
if (codec.rangeOfCodec) {
addCodec(codec.rangeOfCodec);
}
// Tell the system to read the built codec from the registry
Object.defineProperties(codec, {
$exporter$args: { value: [registry, codecName] },
$exporter$factory: {
value: (registry, codecName) => registry.pgCodecs[codecName],
},
});
return codec;
}
}
for (const [executorName, executor] of Object.entries(config.pgExecutors)) {
if (executorName !== executor.name) {
throw new Error(`Executor added to registry with wrong name; ${JSON.stringify(executorName)} !== ${JSON.stringify(executor.name)}`);
}
addExecutor(executor);
}
for (const [codecName, codec] of Object.entries(config.pgCodecs)) {
if (codecName !== codec.name) {
throw new Error(`Codec added to registry with wrong name`);
}
addCodec(codec);
}
for (const [resourceName, rawConfig] of Object.entries(config.pgResources)) {
const resourceConfig = {
...rawConfig,
executor: addExecutor(rawConfig.executor),
codec: addCodec(rawConfig.codec),
parameters: rawConfig.parameters
? rawConfig.parameters.map((p) => ({
...p,
codec: addCodec(p.codec),
}))
: rawConfig.parameters,
};
const resource = new PgResource(registry, resourceConfig);
// This is the magic that breaks the circular reference: rather than
// building PgResource via a factory we tell the system to just retrieve it
// from the already build registry.
Object.defineProperties(resource, {
$exporter$args: { value: [registry, resourceName] },
$exporter$factory: {
value: (registry, resourceName) => registry.pgResources[resourceName],
},
});
registry.pgResources[resourceName] = resource;
}
// Ensure all the relation codecs are also added
for (const codecName of Object.keys(config.pgRelations)) {
const relations = config.pgRelations[codecName];
if (!relations) {
continue;
}
for (const relationName of Object.keys(relations)) {
const relationConfig = relations[relationName];
if (relationConfig) {
addCodec(relationConfig.localCodec);
}
}
}
// DO NOT CALL addCodec BELOW HERE
addCodecForbidden = true;
addExecutorForbidden = true;
/**
* If the user uses a codec with attributes as a column type (or an array of
* the codec is the column type, etc) then we need to have a resource for
* processing this codec. So we add all table-like codecs here, then we
* remove the ones that already have resources, then we build resources for the
* remainder.
*/
const tableLikeCodecsWithoutTableLikeResources = new Set();
const walkCodec = (codec, isAccessibleViaAttribute = false, seen = new Set()) => {
if (seen.has(codec)) {
return;
}
seen.add(codec);
if (isAccessibleViaAttribute &&
codec.attributes &&
codec.executor &&
!codec.isAnonymous) {
tableLikeCodecsWithoutTableLikeResources.add(codec);
}
if (codec.attributes) {
for (const col of Object.values(codec.attributes)) {
if (isAccessibleViaAttribute) {
walkCodec(col.codec, isAccessibleViaAttribute, seen);
}
else {
walkCodec(col.codec, true, new Set());
}
}
}
if (codec.arrayOfCodec) {
walkCodec(codec.arrayOfCodec, isAccessibleViaAttribute, seen);
}
if (codec.rangeOfCodec) {
walkCodec(codec.rangeOfCodec, isAccessibleViaAttribute, seen);
}
if (codec.domainOfCodec) {
walkCodec(codec.domainOfCodec, isAccessibleViaAttribute, seen);
}
};
// Add table-like codecs used within attributes
for (const codec of Object.values(registry.pgCodecs)) {
walkCodec(codec);
}
// Remove from these those codecs that already have resources
for (const resource of Object.values(registry.pgResources)) {
if (!resource.parameters) {
tableLikeCodecsWithoutTableLikeResources.delete(resource.codec);
}
}
// Now add resources for the table-like codecs that don't have them already
for (const codec of tableLikeCodecsWithoutTableLikeResources) {
if (codec.executor) {
const resourceName = `frmcdc_${codec.name}`;
const resource = new PgResource(registry, {
name: resourceName,
executor: codec.executor,
from: (0, pg_sql2_1.default) `(select 1/0 /* codec-only resource; should not select directly */)`,
codec,
identifier: resourceName,
isVirtual: true,
extensions: {
tags: {
behavior: "-*",
},
},
});
Object.defineProperties(resource, {
$exporter$args: { value: [registry, resourceName] },
$exporter$factory: {
value: (registry, resourceName) => registry.pgResources[resourceName],
},
});
registry.pgResources[resourceName] = resource;
}
}
for (const codecName of Object.keys(config.pgRelations)) {
const relations = config.pgRelations[codecName];
if (!relations) {
continue;
}
const builtRelations = Object.create(null);
// Tell the system to read the built relations from the registry
Object.defineProperties(builtRelations, {
$exporter$args: { value: [registry, codecName] },
$exporter$factory: {
value: (registry, codecName) => registry.pgRelations[codecName],
},
});
for (const relationName of Object.keys(relations)) {
const relationConfig = relations[relationName];
if (!relationConfig) {
continue;
}
const { localCodec, remoteResourceOptions, ...rest } = relationConfig;
const builtRelation = {
...rest,
localCodec,
remoteResource: registry.pgResources[remoteResourceOptions.name],
};
// Tell the system to read the built relation from the registry
Object.defineProperties(builtRelation, {
$exporter$args: { value: [registry, codecName, relationName] },
$exporter$factory: {
value: (registry, codecName, relationName) => registry.pgRelations[codecName][relationName],
},
});
builtRelations[relationName] = builtRelation;
}
registry.pgRelations[codecName] = builtRelations;
}
validateRelations(registry);
return registry;
}
(0, grafast_1.exportAs)("@dataplan/pg", makeRegistry, "makeRegistry");
function validateRelations(registry) {
// PERF: skip this if not isDev?
const reg = registry;
for (const codec of Object.values(reg.pgCodecs)) {
// Check that all the `via` and `identicalVia` match actual relations.
const relationKeys = Object.keys(reg.pgRelations[codec.name] ?? {});
if (codec.attributes) {
Object.entries(codec.attributes).forEach(([attributeName, col]) => {
const { via, identicalVia } = col;
if (via != null) {
if (typeof via === "string") {
if (!relationKeys.includes(via)) {
throw new Error(`${codec.name} claims attribute '${attributeName}' is via relation '${via}', but there is no such relation.`);
}
}
else {
if (!relationKeys.includes(via.relation)) {
throw new Error(`${codec.name} claims attribute '${attributeName}' is via relation '${via.relation}', but there is no such relation.`);
}
}
}
if (identicalVia) {
if (typeof identicalVia === "string") {
if (!relationKeys.includes(identicalVia)) {
throw new Error(`${codec.name} claims attribute '${attributeName}' is identicalVia relation '${identicalVia}', but there is no such relation.`);
}
}
else {
if (!relationKeys.includes(identicalVia.relation)) {
throw new Error(`${codec.name} claims attribute '${attributeName}' is identicalVia relation '${identicalVia.relation}', but there is no such relation.`);
}
}
}
});
}
}
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
function makeRegistryBuilder() {
const registryConfig = {
pgExecutors: Object.create(null),
pgCodecs: Object.create(null),
pgResources: Object.create(null),
pgRelations: Object.create(null),
};
const builder = {
getRegistryConfig() {
return registryConfig;
},
addExecutor(executor) {
const existing = registryConfig.pgExecutors[executor.name];
if (existing) {
if (existing !== executor) {
throw new Error(`Attempted to add a second executor named '${executor.name}' (existing: ${(0, inspect_js_1.inspect)(existing)}, new: ${(0, inspect_js_1.inspect)(executor)})`);
}
return builder;
}
registryConfig.pgExecutors[executor.name] = executor;
return builder;
},
addCodec(codec) {
const existing = registryConfig.pgCodecs[codec.name];
if (existing) {
if (existing !== codec) {
throw new Error(`Attempted to add a second codec named '${codec.name}' (existing: ${(0, inspect_js_1.inspect)(existing)}, new: ${(0, inspect_js_1.inspect)(codec)})`);
}
return builder;
}
registryConfig.pgCodecs[codec.name] = codec;
if (codec.arrayOfCodec) {
this.addCodec(codec.arrayOfCodec);
}
if (codec.domainOfCodec) {
this.addCodec(codec.domainOfCodec);
}
if (codec.rangeOfCodec) {
this.addCodec(codec.rangeOfCodec);
}
if (codec.attributes) {
for (const col of Object.values(codec.attributes)) {
this.addCodec(col.codec);
}
}
return builder;
},
addResource(resource) {
this.addExecutor(resource.executor);
const existing = registryConfig.pgResources[resource.name];
if (existing) {
if (existing !== resource) {
throw new Error(`Attempted to add a second resource named '${resource.name}':\n First represented ${printResourceFrom(existing)}.\n Second represents ${printResourceFrom(resource)}.\n Details: ${chalk_1.default.bold.blue.underline `https://err.red/p2rc`}`);
}
return builder;
}
this.addCodec(resource.codec);
registryConfig.pgResources[resource.name] = resource;
return builder;
},
addRelation(localCodec, relationName, remoteResourceOptions, relation) {
if (!registryConfig.pgCodecs[localCodec.name]) {
throw new Error(`Adding a relation before adding the codec is forbidden.`);
}
if (!registryConfig.pgResources[remoteResourceOptions.name]) {
throw new Error(`Adding a relation before adding the resource is forbidden.`);
}
if (!registryConfig.pgRelations[localCodec.name]) {
registryConfig.pgRelations[localCodec.name] = Object.create(null);
}
registryConfig.pgRelations[localCodec.name][relationName] = {
localCodec,
remoteResourceOptions,
...relation,
};
return builder;
},
build() {
return EXPORTABLE((makeRegistry, registryConfig) => makeRegistry(registryConfig), [makeRegistry, registryConfig], "registry");
},
};
return builder;
}
(0, grafast_1.exportAs)("@dataplan/pg", makeRegistryBuilder, "makeRegistryBuilder");
function makePgResourceOptions(options) {
return options;
}
(0, grafast_1.exportAs)("@dataplan/pg", makePgResourceOptions, "makePgResourceOptions");
function printResourceFrom(resource) {
if (typeof resource.from === "function") {
return `a function accepting ${resource.parameters?.length} parameters and returning SQL type '${pg_sql2_1.default.compile(resource.codec.sqlType).text}'`;
}
else {
return `a table/view/etc called '${pg_sql2_1.default.compile(resource.from).text}'`;
}
}
//# sourceMappingURL=datasource.js.map
;