,
): object =>
new Proxy(
{},
{
get: (_target: object, prop: string) => {
const instance = containers[prop];
if (instance) return bindLayerMethods(instance);
return new Proxy(
{},
{
get: (_target2: object, prop2: string) => {
const late = containers[prop];
if (!late) {
throw new Error(
`extension.import('${layer}:${prop}') missing property '${String(prop2)}'`,
);
}
return bindLayerMethods(late)[
prop2 as keyof typeof late
];
},
},
);
},
},
);
export const extension = {
// -- Config access -----------------------------------------------
//
// Lazy proxy to the server config. Populated by PuterServer during
// boot, so extensions can read it at request time (not import time).
get config(): IConfig {
return configContainer;
},
// -- Event subscription -------------------------------------------
on: (
key: P,
callback: (
key: MatchingEvents
,
data: EventMap[MatchingEvents
],
meta: EventMetadata,
) => Promise | void,
) => {
if (!extensionStore.events[key]) {
extensionStore.events[key] = [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extensionStore.events[key].push(callback as any);
},
// -- Registry writers ---------------------------------------------
registerClient: (
name: string,
client: IPuterClientRegistry[keyof IPuterClientRegistry],
) => {
extensionStore.clients[name] = client;
},
registerStore: (
name: string,
store: IPuterStoreRegistry[keyof IPuterStoreRegistry],
) => {
extensionStore.stores[name] = store;
},
registerService: (
name: string,
service: IPuterServiceRegistry[keyof IPuterServiceRegistry],
) => {
extensionStore.services[name] = service;
},
registerController: (
name: string,
controller: IPuterControllerRegistry[keyof IPuterControllerRegistry],
) => {
extensionStore.controllers[name] = controller;
},
registerDriver: (
name: string,
driver: IPuterDriverRegistry[keyof IPuterDriverRegistry],
) => {
extensionStore.drivers[name] = driver;
},
registerGlobalMiddleware: (middleware: RequestHandler) => {
extensionStore.globalMiddlewares.push(middleware);
},
// -- Route registration -------------------------------------------
//
// Supports two call shapes per verb:
// extension.get('/path', handler)
// extension.get('/path', options, handler)
//
// The `options` object is the same `RouteOptions` shape controllers use,
// so everything that works on a controller route (subdomain, requireAuth,
// requireUserActor, adminOnly, allowedAppIds, middleware, bodyJson,
// bodyRaw, bodyText, bodyUrlencoded) works here identically.
get: makeRouteFn('get'),
post: makeRouteFn('post'),
put: makeRouteFn('put'),
delete: makeRouteFn('delete'),
patch: makeRouteFn('patch'),
head: makeRouteFn('head'),
options: makeRouteFn('options'),
all: makeRouteFn('all'),
use: makeUseFn(),
// -- Import proxy -------------------------------------------------
import: (
name: S,
): S extends 'client' | 'clients'
? LayerInstances & IExtensionClientInstances
: S extends 'store' | 'stores'
? LayerInstances & IExtensionStoreInstances
: S extends 'service' | 'services'
? LayerInstances & IExtensionServiceInstances
: S extends 'controller' | 'controllers'
? LayerInstances &
IExtensionControllerInstances
: S extends 'driver' | 'drivers'
? LayerInstances &
IExtensionDriverInstances
: never => {
switch (name) {
case 'clients':
case 'client':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return makeLayerImportProxy('client', clientsContainers) as any;
case 'stores':
case 'store':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return makeLayerImportProxy('store', storesContainers) as any;
case 'services':
case 'service':
return makeLayerImportProxy(
'service',
servicesContainers,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any;
case 'controllers':
case 'controller':
return makeLayerImportProxy(
'controller',
controllersContainers,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any;
case 'drivers':
case 'driver':
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return makeLayerImportProxy('driver', driversContainers) as any;
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return undefined as any;
}
},
};