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
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);
|
|
};
|
|
}
|
|
};
|
|
};
|