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.
129 lines
4.1 KiB
JavaScript
129 lines
4.1 KiB
JavaScript
"use strict";
|
|
|
|
// const mergeByTemplate = require("merge-by-template");
|
|
const syncpipe = require("syncpipe");
|
|
const deepMerge = require("./deep-merge");
|
|
|
|
/*
|
|
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] = {
|
|
source: module,
|
|
// No context provided to type factory functions for now, since they are not allowed to be async for now anyway
|
|
// FIXME: Maybe add a warning if the user returns a Promise from a factory, asking them to file a bug if they really need it?
|
|
// func: wrapModuleFunction(module, factory)
|
|
func: factory
|
|
};
|
|
}
|
|
},
|
|
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 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
|
|
|
|
let types = createTypeTracker();
|
|
let typeExtensions = createExtensionTracker();
|
|
|
|
let contextFactories = syncpipe(modules, [
|
|
_ => _.map((module) => [ getModuleID(module), module.makeContext ?? defaultContext ]),
|
|
_ => new Map(_)
|
|
]);
|
|
|
|
let schemaRoots = modules.map((module) => module.root ?? {});
|
|
|
|
for (let module of modules) {
|
|
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: schemaRoots.reduce(deepMerge, {}),
|
|
types: types.get(),
|
|
extensions: typeExtensions.get(),
|
|
makeContextFactory: function (baseContext) {
|
|
let cache = new Map();
|
|
|
|
return function makeContextForModule(moduleID) {
|
|
if (!cache.has(moduleID)) {
|
|
cache.set(moduleID, {
|
|
... baseContext,
|
|
... contextFactories.get(moduleID)()
|
|
});
|
|
}
|
|
|
|
return cache.get(moduleID);
|
|
};
|
|
}
|
|
};
|
|
};
|