@pikku/core 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## 0.7.0
2
2
 
3
+ ## 0.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 534fdef: feat: adding rpc (locally for now)
8
+
9
+ ## 0.7.2
10
+
11
+ ### Patch Changes
12
+
13
+ - bb59874: fix: only try validating schemas if they exist in function runner
14
+
3
15
  ## 0.7.1
4
16
 
5
17
  ### Patch Changes
@@ -47,7 +47,6 @@ export const processMessageHandlers = (services, session, channelConfig, channel
47
47
  const { pikkuFuncName } = getRouteMeta(channelConfig.name, routingProperty, routerValue);
48
48
  const permissions = typeof onMessage === 'function' ? {} : onMessage.permissions;
49
49
  return await runPikkuFunc(pikkuFuncName, {
50
- singletonServices: services,
51
50
  getAllServices: () => ({
52
51
  ...services,
53
52
  channel: channelHandler.getChannel(),
@@ -17,18 +17,24 @@ export const addChannel = (channel) => {
17
17
  pikkuState('channel', 'channels').set(channel.name, channel);
18
18
  // Register onConnect function if provided
19
19
  if (channel.onConnect && channelMeta.connectPikkuFuncName) {
20
- addFunction(channelMeta.connectPikkuFuncName, channel.onConnect);
20
+ addFunction(channelMeta.connectPikkuFuncName, {
21
+ func: channel.onConnect,
22
+ });
21
23
  }
22
24
  // Register onDisconnect function if provided
23
25
  if (channel.onDisconnect && channelMeta.disconnectPikkuFuncName) {
24
- addFunction(channelMeta.disconnectPikkuFuncName, channel.onDisconnect);
26
+ addFunction(channelMeta.disconnectPikkuFuncName, {
27
+ func: channel.onDisconnect,
28
+ });
25
29
  }
26
30
  // Register onMessage function if provided
27
31
  if (channel.onMessage && channelMeta.message?.pikkuFuncName) {
28
32
  const messageFunc = typeof channel.onMessage === 'function'
29
33
  ? channel.onMessage
30
34
  : channel.onMessage.func;
31
- addFunction(channelMeta.message.pikkuFuncName, messageFunc);
35
+ addFunction(channelMeta.message.pikkuFuncName, {
36
+ func: messageFunc,
37
+ });
32
38
  }
33
39
  // Register functions in onMessageRoute
34
40
  if (channel.onMessageRoute && channelMeta.messageRoutes) {
@@ -43,7 +49,13 @@ export const addChannel = (channel) => {
43
49
  if (!routeMeta)
44
50
  return;
45
51
  // Extract the function from the handler
46
- const routeFunc = typeof handler === 'function' ? handler : handler.func;
52
+ const routeFunc = typeof handler === 'function'
53
+ ? { func: handler }
54
+ : {
55
+ func: handler.func,
56
+ auth: handler.auth,
57
+ permissions: handler.permissions,
58
+ };
47
59
  // Register the function using the pikku name from metadata
48
60
  addFunction(routeMeta.pikkuFuncName, routeFunc);
49
61
  });
@@ -7,6 +7,7 @@ import { handleError } from '../../handle-error.js';
7
7
  import { runMiddleware } from '../../middleware-runner.js';
8
8
  import { PikkuUserSessionService } from '../../services/user-session-service.js';
9
9
  import { runPikkuFuncDirectly } from '../../function/function-runner.js';
10
+ import { rpcService } from '../../rpc/rpc-runner.js';
10
11
  export const runLocalChannel = async ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession = false, respondWith404 = true, coerceDataFromSchema = true, logWarningsForStatusCodes = [], bubbleErrors = false, }) => {
11
12
  let sessionServices;
12
13
  let channelHandler;
@@ -36,11 +37,11 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
36
37
  if (createSessionServices) {
37
38
  sessionServices = await createSessionServices(singletonServices, { http }, session);
38
39
  }
39
- const allServices = {
40
+ const allServices = rpcService.injectRPCService({
40
41
  ...singletonServices,
41
42
  ...sessionServices,
42
43
  userSession: userSession,
43
- };
44
+ });
44
45
  channelHandler.registerOnOpen(() => {
45
46
  if (channelConfig.onConnect && meta.connectPikkuFuncName) {
46
47
  runPikkuFuncDirectly(meta.connectPikkuFuncName, { ...allServices, channel }, openingData);
@@ -1,9 +1,8 @@
1
- import { CoreServices, CoreSingletonServices, CoreUserSession } from '../types/core.types.js';
2
- import { CoreAPIFunction, CoreAPIFunctionSessionless, CorePermissionGroup } from './functions.types.js';
3
- export declare const addFunction: (funcName: string, func: CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>) => void;
1
+ import { CoreServices, CoreUserSession } from '../types/core.types.js';
2
+ import { CorePermissionGroup, CorePikkuFunctionConfig } from './functions.types.js';
3
+ export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>) => void;
4
4
  export declare const runPikkuFuncDirectly: <In, Out>(funcName: string, allServices: CoreServices, data: In, session?: CoreUserSession) => Promise<Out>;
5
- export declare const runPikkuFunc: <In = any, Out = any>(funcName: string, { singletonServices, getAllServices, data, session, permissions, coerceDataFromSchema, }: {
6
- singletonServices: CoreSingletonServices;
5
+ export declare const runPikkuFunc: <In = any, Out = any>(funcName: string, { getAllServices, data, session, permissions: transportPermissions, coerceDataFromSchema, }: {
7
6
  getAllServices: () => Promise<CoreServices> | CoreServices;
8
7
  data: In;
9
8
  session?: CoreUserSession;
@@ -2,37 +2,79 @@ import { ForbiddenError } from '../errors/errors.js';
2
2
  import { verifyPermissions } from '../permissions.js';
3
3
  import { pikkuState } from '../pikku-state.js';
4
4
  import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
5
- export const addFunction = (funcName, func) => {
6
- pikkuState('functions', 'nameToFunction').set(funcName, func);
5
+ // Permission merging function
6
+ function mergePermissions(logger, funcName, funcPermissions, transportPermissions) {
7
+ if (!funcPermissions && !transportPermissions) {
8
+ return undefined;
9
+ }
10
+ if (!funcPermissions) {
11
+ return transportPermissions;
12
+ }
13
+ if (!transportPermissions) {
14
+ return funcPermissions;
15
+ }
16
+ // Start with a copy of function permissions
17
+ const mergedPermissions = { ...funcPermissions };
18
+ // Merge in transport permissions
19
+ for (const [key, transportValue] of Object.entries(transportPermissions)) {
20
+ if (key in mergedPermissions) {
21
+ // For permission arrays, concatenate and deduplicate values
22
+ if (Array.isArray(mergedPermissions[key]) &&
23
+ Array.isArray(transportValue)) {
24
+ mergedPermissions[key] = [
25
+ ...new Set([...mergedPermissions[key], ...transportValue]),
26
+ ];
27
+ }
28
+ // For other types, warn about conflict and use the more restrictive (transport-level) value
29
+ else {
30
+ logger.warn(`Permission conflict on key "${key}" for function "${funcName}". Using transport-level permission.`);
31
+ mergedPermissions[key] = transportValue;
32
+ }
33
+ }
34
+ else {
35
+ // If key doesn't exist in function permissions, add it
36
+ mergedPermissions[key] = transportValue;
37
+ }
38
+ }
39
+ return mergedPermissions;
40
+ }
41
+ export const addFunction = (funcName, funcConfig) => {
42
+ pikkuState('function', 'functions').set(funcName, funcConfig);
7
43
  };
8
44
  export const runPikkuFuncDirectly = async (funcName, allServices, data, session) => {
9
- const func = pikkuState('functions', 'nameToFunction').get(funcName);
10
- if (!func) {
45
+ const funcConfig = pikkuState('function', 'functions').get(funcName);
46
+ if (!funcConfig) {
11
47
  throw new Error(`Function not found: ${funcName}`);
12
48
  }
13
- return (await func(allServices, data, session));
49
+ return (await funcConfig.func(allServices, data, session));
14
50
  };
15
- export const runPikkuFunc = async (funcName, { singletonServices, getAllServices, data, session, permissions, coerceDataFromSchema, }) => {
16
- const func = pikkuState('functions', 'nameToFunction').get(funcName);
17
- if (!func) {
51
+ export const runPikkuFunc = async (funcName, { getAllServices, data, session, permissions: transportPermissions, coerceDataFromSchema, }) => {
52
+ const funcConfig = pikkuState('function', 'functions').get(funcName);
53
+ if (!funcConfig) {
18
54
  throw new Error(`Function not found: ${funcName}`);
19
55
  }
20
- const funcMeta = pikkuState('functions', 'meta')[funcName];
56
+ const funcMeta = pikkuState('function', 'meta')[funcName];
21
57
  if (!funcMeta) {
22
58
  throw new Error(`Function meta not found: ${funcName}`);
23
59
  }
24
- const schemaName = funcMeta.schemaName;
25
- // Validate request data against the defined schema, if any
26
- await validateSchema(singletonServices.logger, singletonServices.schema, schemaName, data);
27
- // Coerce (top level) query string parameters or date objects if specified by the schema
28
- if (coerceDataFromSchema && schemaName) {
29
- coerceTopLevelDataFromSchema(schemaName, data);
60
+ if (funcConfig.auth && !session) {
61
+ throw new ForbiddenError(`Function ${funcName} requires authentication even though transport does not`);
30
62
  }
31
63
  const allServices = await getAllServices();
64
+ const schemaName = funcMeta.schemaName;
65
+ if (schemaName) {
66
+ // Validate request data against the defined schema, if any
67
+ await validateSchema(allServices.logger, allServices.schema, schemaName, data);
68
+ // Coerce (top level) query string parameters or date objects if specified by the schema
69
+ if (coerceDataFromSchema) {
70
+ coerceTopLevelDataFromSchema(schemaName, data);
71
+ }
72
+ }
32
73
  // Execute permission checks
33
- const permissioned = await verifyPermissions(permissions, allServices, data, session);
74
+ const mergedPermissions = mergePermissions(allServices.logger, funcName, funcConfig.permissions, transportPermissions);
75
+ const permissioned = await verifyPermissions(mergedPermissions, allServices, data, session);
34
76
  if (permissioned === false) {
35
77
  throw new ForbiddenError('Permission denied');
36
78
  }
37
- return (await func(allServices, data, session));
79
+ return (await funcConfig.func(allServices, data, session));
38
80
  };
@@ -35,3 +35,8 @@ export type CoreAPIFunctionSessionless<In, Out, ChannelData extends unknown | nu
35
35
  */
36
36
  export type CoreAPIPermission<In = any, Services extends CoreSingletonServices = CoreServices, Session extends CoreUserSession = CoreUserSession> = (services: Services, data: In, session?: Session) => Promise<boolean>;
37
37
  export type CorePermissionGroup<APIPermission = CoreAPIPermission<any>> = Record<string, APIPermission | APIPermission[]> | undefined;
38
+ export type CorePikkuFunctionConfig<APIFunction extends CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>, APIPermission extends CoreAPIPermission<any> = CoreAPIPermission<any>> = {
39
+ func: APIFunction;
40
+ auth?: boolean;
41
+ permissions?: CorePermissionGroup<APIPermission>;
42
+ };
@@ -8,6 +8,7 @@ import { pikkuState } from '../pikku-state.js';
8
8
  import { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js';
9
9
  import { PikkuFetchHTTPRequest } from './pikku-fetch-http-request.js';
10
10
  import { addFunction, runPikkuFunc } from '../function/function-runner.js';
11
+ import { rpcService } from '../rpc/rpc-runner.js';
11
12
  /**
12
13
  * Registers middleware either globally or for a specific route.
13
14
  *
@@ -54,7 +55,11 @@ export const addHTTPRoute = (httpRoute) => {
54
55
  if (!routeMeta) {
55
56
  throw new Error('Route metadata not found');
56
57
  }
57
- addFunction(routeMeta.pikkuFuncName, httpRoute.func);
58
+ addFunction(routeMeta.pikkuFuncName, {
59
+ func: httpRoute.func,
60
+ auth: httpRoute.auth,
61
+ permissions: httpRoute.permissions,
62
+ });
58
63
  const routes = pikkuState('http', 'routes');
59
64
  if (!routes.has(httpRoute.method)) {
60
65
  routes.set(httpRoute.method, new Map());
@@ -204,16 +209,15 @@ const executeRouteWithMiddleware = async (services, matchedRoute, http, options)
204
209
  }
205
210
  // Create session-specific services for handling the request
206
211
  sessionServices = await createSessionServices({ ...singletonServices, userSession, channel }, { http }, session);
207
- return {
212
+ return rpcService.injectRPCService({
208
213
  ...singletonServices,
209
214
  ...sessionServices,
210
215
  http,
211
216
  userSession,
212
217
  channel,
213
- };
218
+ });
214
219
  };
215
220
  const result = await runPikkuFunc(meta.pikkuFuncName, {
216
- singletonServices,
217
221
  getAllServices,
218
222
  session,
219
223
  data,
@@ -3,11 +3,15 @@ import { CoreHTTPFunctionRoute, HTTPMethod, HTTPRoutesMeta } from './http/http.t
3
3
  import { FunctionsMeta, PikkuMiddleware } from './types/core.types.js';
4
4
  import { CoreScheduledTask, ScheduledTasksMeta } from './scheduler/scheduler.types.js';
5
5
  import { ErrorDetails, PikkuError } from './errors/error-handler.js';
6
- import { CoreAPIFunction, CoreAPIFunctionSessionless } from './function/functions.types.js';
6
+ import { CorePikkuFunctionConfig } from './function/functions.types.js';
7
+ import { RPCMeta } from './rpc/rpc-types.js';
7
8
  interface PikkuState {
8
- functions: {
9
+ function: {
9
10
  meta: FunctionsMeta;
10
- nameToFunction: Map<string, CoreAPIFunction<any, any> | CoreAPIFunctionSessionless<any, any>>;
11
+ functions: Map<string, CorePikkuFunctionConfig<any, any>>;
12
+ };
13
+ rpc: {
14
+ meta: Record<string, RPCMeta>;
11
15
  };
12
16
  http: {
13
17
  middleware: Array<{
@@ -1,8 +1,11 @@
1
1
  export const resetPikkuState = () => {
2
2
  globalThis.pikkuState = {
3
- functions: {
3
+ function: {
4
+ meta: {},
5
+ functions: new Map(),
6
+ },
7
+ rpc: {
4
8
  meta: {},
5
- nameToFunction: new Map(),
6
9
  },
7
10
  http: {
8
11
  middleware: [],
@@ -0,0 +1 @@
1
+ export { initialize } from './rpc-runner.js';
@@ -0,0 +1 @@
1
+ export { initialize } from './rpc-runner.js';
@@ -0,0 +1,31 @@
1
+ import { CoreSingletonServices, CoreServices, CreateSessionServices, CoreUserSession } from '../types/core.types.js';
2
+ type RPCServiceConfig = {
3
+ singletonServices: CoreSingletonServices;
4
+ createSessionServices: CreateSessionServices;
5
+ coerceDataFromSchema: boolean;
6
+ };
7
+ export declare class PikkuRPCService {
8
+ private config?;
9
+ initialize(config: RPCServiceConfig): void;
10
+ injectRPCService(coreServices: CoreServices, depth?: number): {
11
+ jwt?: import("../index.js").JWTService;
12
+ schema?: import("../index.js").SchemaService;
13
+ config: {
14
+ logLevel?: import("../index.js").LogLevel;
15
+ secrets?: {};
16
+ };
17
+ logger: import("../index.js").Logger;
18
+ variables: import("../index.js").VariablesService;
19
+ eventHub?: import("../index.js").EventHubService<unknown>;
20
+ secrets?: import("../index.js").SecretService;
21
+ http?: import("../index.js").PikkuHTTP;
22
+ rpc?: import("./rpc-types.js").PikkuRPC;
23
+ userSession?: import("../services/user-session-service.js").UserSessionService<CoreUserSession> | undefined;
24
+ channel?: import("../index.js").PikkuChannel<unknown, unknown>;
25
+ };
26
+ rpc<In = any, Out = any>(funcName: string, data: In, session?: CoreUserSession): Promise<Out>;
27
+ }
28
+ export declare const rpcService: PikkuRPCService;
29
+ export declare const initialize: (config: RPCServiceConfig) => void;
30
+ export declare const pikkuRPC: <In = any, Out = any, session = CoreUserSession>(funcName: string, data: In, session?: CoreUserSession) => Promise<Out>;
31
+ export {};
@@ -0,0 +1,101 @@
1
+ import { runPikkuFunc } from '../function/function-runner.js';
2
+ import { pikkuState } from '../pikku-state.js';
3
+ import { PikkuUserSessionService } from '../services/user-session-service.js';
4
+ const getRPCMeta = (rpcName) => {
5
+ const rpc = pikkuState('rpc', 'meta');
6
+ const rpcMeta = rpc[rpcName];
7
+ if (!rpcMeta) {
8
+ throw new Error(`RPC function not found: ${rpcName}`);
9
+ }
10
+ return rpcMeta;
11
+ };
12
+ // Context-aware RPC client for use within services
13
+ class ServiceRPC {
14
+ services;
15
+ options;
16
+ constructor(services, options) {
17
+ this.services = services;
18
+ this.options = options;
19
+ }
20
+ async rpc(funcName, data) {
21
+ const session = await this.services.userSession?.get();
22
+ const rpcDepth = this.services.rpc?.depth || 0;
23
+ const rpcMeta = getRPCMeta(funcName);
24
+ return runPikkuFunc(rpcMeta.pikkuFuncName, {
25
+ getAllServices: () => {
26
+ this.services.rpc = this.services.rpc
27
+ ? {
28
+ ...this.services.rpc,
29
+ depth: rpcDepth + 1,
30
+ global: false,
31
+ }
32
+ : undefined;
33
+ return this.services;
34
+ },
35
+ data,
36
+ session,
37
+ coerceDataFromSchema: this.options.coerceDataFromSchema,
38
+ });
39
+ }
40
+ }
41
+ // RPC Service class for the global interface
42
+ export class PikkuRPCService {
43
+ config;
44
+ // Initialize the RPC service with configuration
45
+ initialize(config) {
46
+ this.config = config;
47
+ }
48
+ // Convenience function for initializing
49
+ injectRPCService(coreServices, depth = 0) {
50
+ const serviceCopy = {
51
+ ...coreServices,
52
+ };
53
+ const serviceRPC = new ServiceRPC(serviceCopy, {
54
+ coerceDataFromSchema: this.config?.coerceDataFromSchema,
55
+ });
56
+ serviceCopy.rpc = {
57
+ depth,
58
+ global: false,
59
+ invoke: serviceRPC.rpc.bind(serviceRPC),
60
+ };
61
+ return serviceCopy;
62
+ }
63
+ // Global RPC method to call functions by name
64
+ async rpc(funcName, data, session) {
65
+ if (!this.config) {
66
+ throw new Error('RPC service not initialized');
67
+ }
68
+ const rpcMeta = getRPCMeta(funcName);
69
+ const { singletonServices, createSessionServices, coerceDataFromSchema } = this.config;
70
+ // Define the getAllServices function for runPikkuFunc
71
+ const getAllServices = async () => {
72
+ const userSession = new PikkuUserSessionService();
73
+ if (session) {
74
+ userSession.set(session);
75
+ }
76
+ const sessionServices = await createSessionServices(singletonServices, {}, session);
77
+ return this.injectRPCService({
78
+ ...singletonServices,
79
+ ...sessionServices,
80
+ userSession,
81
+ }, 0);
82
+ };
83
+ // Call the function using runPikkuFunc - it will handle permission merging
84
+ return runPikkuFunc(rpcMeta.pikkuFuncName, {
85
+ getAllServices,
86
+ data,
87
+ session,
88
+ coerceDataFromSchema: coerceDataFromSchema ?? true,
89
+ });
90
+ }
91
+ }
92
+ // Create a singleton instance
93
+ export const rpcService = new PikkuRPCService();
94
+ // Convenience function for initializing
95
+ export const initialize = (config) => {
96
+ rpcService.initialize(config);
97
+ };
98
+ // Convenience function for making standalone RPC calls
99
+ export const pikkuRPC = (funcName, data, session) => {
100
+ return rpcService.rpc(funcName, data, session);
101
+ };
@@ -0,0 +1,9 @@
1
+ export type PikkuRPC = {
2
+ depth: number;
3
+ global: boolean;
4
+ invoke: any;
5
+ };
6
+ export type RPCMeta = {
7
+ pikkuFuncName: string;
8
+ exposed: boolean;
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -2,13 +2,16 @@ import { getErrorResponse } from '../errors/error-handler.js';
2
2
  import { closeSessionServices } from '../utils.js';
3
3
  import { pikkuState } from '../pikku-state.js';
4
4
  import { addFunction, runPikkuFunc } from '../function/function-runner.js';
5
+ import { rpcService } from '../rpc/rpc-runner.js';
5
6
  export const addScheduledTask = (scheduledTask) => {
6
7
  const meta = pikkuState('scheduler', 'meta');
7
8
  const taskMeta = meta[scheduledTask.name];
8
9
  if (!taskMeta) {
9
10
  throw new Error('Task metadata not found');
10
11
  }
11
- addFunction(taskMeta.pikkuFuncName, scheduledTask.func);
12
+ addFunction(taskMeta.pikkuFuncName, {
13
+ func: scheduledTask.func,
14
+ });
12
15
  const tasks = pikkuState('scheduler', 'tasks');
13
16
  if (tasks.has(scheduledTask.name)) {
14
17
  throw new Error(`Scheduled task already exists: ${scheduledTask.name}`);
@@ -35,12 +38,14 @@ export async function runScheduledTask({ name, session, singletonServices, creat
35
38
  const getAllServices = async () => {
36
39
  if (createSessionServices) {
37
40
  const sessionServices = await createSessionServices(singletonServices, {}, session);
38
- return { ...singletonServices, ...sessionServices };
41
+ return rpcService.injectRPCService({
42
+ ...singletonServices,
43
+ ...sessionServices,
44
+ });
39
45
  }
40
46
  return singletonServices;
41
47
  };
42
48
  return await runPikkuFunc(meta.pikkuFuncName, {
43
- singletonServices,
44
49
  getAllServices,
45
50
  session,
46
51
  data: undefined,
@@ -7,6 +7,7 @@ import { UserSessionService } from '../services/user-session-service.js';
7
7
  import { JWTService } from '../services/jwt-service.js';
8
8
  import { SecretService } from '../services/secret-service.js';
9
9
  import { PikkuChannel } from '../channel/channel.types.js';
10
+ import { PikkuRPC } from '../rpc/rpc-types.js';
10
11
  export interface FunctionServicesMeta {
11
12
  optimized: boolean;
12
13
  services: string[];
@@ -93,6 +94,7 @@ export type PikkuMiddleware<SingletonServices extends CoreSingletonServices = Co
93
94
  * Represents the core services used by Pikku, including singleton services and the request/response interaction.
94
95
  */
95
96
  export type CoreServices<SingletonServices = CoreSingletonServices, UserSession extends CoreUserSession = CoreUserSession, CoreServices extends Record<string, unknown> = {}> = SingletonServices & PikkuInteraction & {
97
+ rpc?: PikkuRPC;
96
98
  userSession?: UserSessionService<UserSession>;
97
99
  channel?: PikkuChannel<unknown, unknown>;
98
100
  } & CoreServices;