You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

160 lines
5.2 KiB
JavaScript

"use strict";
// const mergeByTemplate = require("merge-by-template");
const syncpipe = require("syncpipe");
const mapObject = require("map-obj");
const deepMergeAndMap = require("./deep-merge-and-map");
const InvalidObject = require("./invalid-object");
/*
Take a list of modules; each module specifies a name, schema root, types, type extensions, and a context factory function. Each module is internally assigned a unique ID. This unique ID is associated with each type factory and type extension method, and used as the key for a map of context factories; that way, upon invoking those methods, the module's own corresponding context can be injected. Only a single context should be created per module per request, so there should be a cache layer for the contexts (keyed by module ID), with each request creating a new cache.
*/
// NOTE: This can be global because we identify existing assignments by object identity, and that will never conflict
let numberedModules = new WeakMap();
let currentModuleNumber = 0;
function getModuleID(module) {
if (!numberedModules.has(module)) {
numberedModules.set(module, currentModuleNumber++);
}
return numberedModules.get(module);
}
function createTypeTracker() {
let typeFactories = {};
return {
add: function (module, name, factory) {
if (typeFactories[name] != null) {
let existingEntry = typeFactories[name];
throw new Error(`Type '${name}' already exists (from module '${module.name}', already defined by module '${existingEntry.source.name}')`);
} else {
typeFactories[name] = {
__moduleID: getModuleID(module),
source: module,
// func: wrapModuleFunction(module, factory)
func: async function (args, context) {
// NOTE: We pass through the context here without modifying it; because a module-specific context will be injected from the main runtime
let instance = await factory(args, context);
if (instance !== InvalidObject) {
// We need to patch every created object, so that the correct context gets injected into its type-specific methods when they are called
return mapObject(instance, (key, value) => {
return [ key, wrapModuleValue(module, value) ];
});
} else {
return instance;
}
}
};
}
},
get: function () {
return typeFactories;
}
};
}
function createExtensionTracker() {
let extendedTypes = {};
return {
add: function (module, type, name, method) {
if (extendedTypes[type] == null) {
extendedTypes[type] = {};
}
let extensions = extendedTypes[type];
if (extensions[name] != null) {
let existingEntry = extensions[name];
throw new Error(`Type '${type}' already has a method extension named '${name}' (from module '${module.name}', already defined by module '${existingEntry.source.name}')`);
} else {
extensions[name] = {
source: module,
func: wrapModuleFunction(module, method)
};
}
},
get: function () {
return extendedTypes;
}
};
}
function wrapModuleFunction(module, func) {
return { __moduleID: getModuleID(module), func: func };
}
function wrapModuleValue(module, value) {
if (typeof value === "function") {
return wrapModuleFunction(module, value);
} else {
return value;
}
}
function defaultContext() {
// Fallback function that generates an empty context, for when a module doesn't specify a makeContext handler
return {};
}
module.exports = function (modules) {
// TODO: Eventually replace hand-crafted merging logic with merge-by-template, once it can support this usecase properly(tm)
// TODO: Fix merge-by-template so that reasonable error messages can be generated here, that are actually aware of eg. the conflicting key
// TODO: Disallow modules with duplicate names? This may be necessary to prevent issues with getModuleContext, which provides name-based context access
let types = createTypeTracker();
let typeExtensions = createExtensionTracker();
let nameToID = new Map(); // TODO: Make this a tracker abstraction?
let contextFactories = syncpipe(modules, [
_ => _.map((module) => [ getModuleID(module), module.makeContext ?? defaultContext ]),
_ => new Map(_)
]);
let schema = modules.reduce((schema, module) => {
return deepMergeAndMap(schema, module.root, (value) => wrapModuleValue(module, value));
}, {});
for (let module of modules) {
if (module.name != null) {
nameToID.set(module.name, getModuleID(module));
}
for (let [ type, factory ] of Object.entries(module.types ?? {})) {
types.add(module, type, factory);
}
for (let [ type, extensions ] of Object.entries(module.extensions ?? {})) {
for (let [ name, method ] of Object.entries(extensions)) {
typeExtensions.add(module, type, name, method);
}
}
}
return {
root: schema,
types: types.get(),
extensions: typeExtensions.get(),
nameToID: nameToID,
makeContextFactory: function (baseContext) {
let cache = new Map();
return function makeContextForModule(moduleID) {
if (!cache.has(moduleID)) {
cache.set(moduleID, {
... baseContext,
// NOTE: Module context initialization gets access to the baseContext, for user-level overrides (eg. passing in transaction objects for a query)
... contextFactories.get(moduleID)(baseContext)
});
}
return cache.get(moduleID);
};
}
};
};