@pikku/cli 0.9.5 → 0.9.6

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,18 @@
1
1
  # @pikku/cli
2
2
 
3
+ ## 0.9.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 6059c87: refactor: move PikkuPermission to pikkuPermission and same for middleware for api consistency to to improve future features
8
+ - 6db63bb: perf: changing http meta to a lookup map to reduce loops
9
+ - Updated dependencies [6059c87]
10
+ - Updated dependencies [6db63bb]
11
+ - Updated dependencies [74f8634]
12
+ - Updated dependencies [766fef1]
13
+ - @pikku/inspector@0.9.4
14
+ - @pikku/core@0.9.6
15
+
3
16
  ## 0.9.5
4
17
 
5
18
  ### Patch Changes
@@ -12,15 +12,17 @@ export async function generateSchemas(logger, tsconfig, typesMap, functionMeta,
12
12
  }
13
13
  }
14
14
  }
15
- for (const { inputTypes } of httpWiringsMeta) {
16
- if (inputTypes?.body) {
17
- schemasSet.add(inputTypes.body);
18
- }
19
- if (inputTypes?.query) {
20
- schemasSet.add(inputTypes.query);
21
- }
22
- if (inputTypes?.params) {
23
- schemasSet.add(inputTypes.params);
15
+ for (const wiringRoutes of Object.values(httpWiringsMeta)) {
16
+ for (const { inputTypes } of Object.values(wiringRoutes)) {
17
+ if (inputTypes?.body) {
18
+ schemasSet.add(inputTypes.body);
19
+ }
20
+ if (inputTypes?.query) {
21
+ schemasSet.add(inputTypes.query);
22
+ }
23
+ if (inputTypes?.params) {
24
+ schemasSet.add(inputTypes.params);
25
+ }
24
26
  }
25
27
  }
26
28
  const generator = createGenerator({
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  *
3
3
  */
4
- export declare const serializePikkuTypes: (userSessionTypeImport: string, userSessionTypeName: string, singletonServicesTypeImport: string, singletonServicesTypeName: string, sessionServicesTypeImport: string, rpcMapTypeImport: string) => string;
4
+ export declare const serializePikkuTypes: (userSessionTypeImport: string, userSessionTypeName: string, singletonServicesTypeImport: string, singletonServicesTypeName: string, sessionServicesTypeImport: string, sessionServicesTypeName: string, rpcMapTypeImport: string) => string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  *
3
3
  */
4
- export const serializePikkuTypes = (userSessionTypeImport, userSessionTypeName, singletonServicesTypeImport, singletonServicesTypeName, sessionServicesTypeImport, rpcMapTypeImport) => {
4
+ export const serializePikkuTypes = (userSessionTypeImport, userSessionTypeName, singletonServicesTypeImport, singletonServicesTypeName, sessionServicesTypeImport, sessionServicesTypeName, rpcMapTypeImport) => {
5
5
  return `/**
6
6
  * This is used to provide the application types in the typescript project
7
7
  */
@@ -19,6 +19,10 @@ ${singletonServicesTypeImport}
19
19
  ${sessionServicesTypeImport}
20
20
  ${rpcMapTypeImport}
21
21
 
22
+ ${singletonServicesTypeName !== 'SingletonServices' ? `type SingletonServices = ${singletonServicesTypeName}` : ''}
23
+ ${sessionServicesTypeName !== 'Services' ? `type Services = ${sessionServicesTypeName}` : ''}
24
+ ${userSessionTypeName !== 'Session' ? `type Session = ${userSessionTypeName}` : ''}
25
+
22
26
  /**
23
27
  * Type-safe API permission definition that integrates with your application's session type.
24
28
  * Use this to define authorization logic for your API endpoints.
@@ -26,7 +30,7 @@ ${rpcMapTypeImport}
26
30
  * @template In - The input type that the permission check will receive
27
31
  * @template RequiredServices - The services required for this permission check
28
32
  */
29
- export type PikkuPermission<In = unknown, RequiredServices extends ${singletonServicesTypeName} = ${singletonServicesTypeName}> = CorePikkuPermission<In, RequiredServices, ${userSessionTypeName}>
33
+ type PikkuPermission<In = unknown, RequiredServices extends Services = Services> = CorePikkuPermission<In, RequiredServices, Session>
30
34
 
31
35
  /**
32
36
  * Type-safe middleware definition that can access your application's services and session.
@@ -34,7 +38,38 @@ export type PikkuPermission<In = unknown, RequiredServices extends ${singletonSe
34
38
  *
35
39
  * @template RequiredServices - The services required for this middleware
36
40
  */
37
- export type PikkuMiddleware<RequiredServices extends ${singletonServicesTypeName} = ${singletonServicesTypeName}> = CorePikkuMiddleware<RequiredServices, ${userSessionTypeName}>
41
+ type PikkuMiddleware<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuMiddleware<RequiredServices, Session>
42
+
43
+ /**
44
+ * Factory function for creating permissions with tree-shaking support.
45
+ * This enables the bundler to detect which services your permission actually uses.
46
+ *
47
+ * @example
48
+ * \`\`\`typescript
49
+ * const permission = pikkuPermission(({ logger }, data, session) => {
50
+ * return session?.isAdmin || false
51
+ * })
52
+ * \`\`\`
53
+ */
54
+ export const pikkuPermission = <In>(func: PikkuPermission<In>) => {
55
+ return func
56
+ }
57
+
58
+ /**
59
+ * Factory function for creating middleware with tree-shaking support.
60
+ * This enables the bundler to detect which services your middleware actually uses.
61
+ *
62
+ * @example
63
+ * \`\`\`typescript
64
+ * const middleware = pikkuMiddleware(({ logger }, interactions, next) => {
65
+ * logger.info('Middleware executed')
66
+ * await next()
67
+ * })
68
+ * \`\`\`
69
+ */
70
+ export const pikkuMiddleware = (func: PikkuMiddleware) => {
71
+ return func
72
+ }
38
73
 
39
74
  /**
40
75
  * A sessionless API function that doesn't require user authentication.
@@ -60,7 +95,7 @@ type PikkuFunctionSessionless<
60
95
  ? { mcp?: PikkuMCP } // Optional MCP
61
96
  : { mcp: PikkuMCP } // Required MCP
62
97
  )
63
- > = CorePikkuFunctionSessionless<In, Out, ChannelData, RequiredServices, ${userSessionTypeName}>
98
+ > = CorePikkuFunctionSessionless<In, Out, ChannelData, RequiredServices, Session>
64
99
 
65
100
  /**
66
101
  * A session-aware API function that requires user authentication.
@@ -86,7 +121,7 @@ type PikkuFunction<
86
121
  ? { mcp?: PikkuMCP } // Optional MCP
87
122
  : { mcp: PikkuMCP } // Required MCP
88
123
  )
89
- > = CorePikkuFunction<In, Out, ChannelData, RequiredServices, ${userSessionTypeName}>
124
+ > = CorePikkuFunction<In, Out, ChannelData, RequiredServices, Session>
90
125
 
91
126
  /**
92
127
  * Type definition for HTTP API wirings with type-safe path parameters.
@@ -516,7 +551,7 @@ export const pikkuMCPPromptFunc = <In>(
516
551
  * const results = await fileSystem.search(input.query, input.directory)
517
552
  * return [{
518
553
  * type: 'text',
519
- * text: \`Found \${results.length} files matching \"\${input.query}\"\`
554
+ * text: \`Found \${results.length} files matching "\${input.query}"\`
520
555
  * }]
521
556
  * }
522
557
  * })
package/dist/src/utils.js CHANGED
@@ -10,7 +10,7 @@ const logo = `
10
10
  _____) )| | _| | _ _ _
11
11
  | ____/ | |_/ ) |_/ ) | | |
12
12
  | | | | _ (| _ (| |_| |
13
- |_| |_|_| \_)_| \_)____/
13
+ |_| |_|_| _)_| _)____/
14
14
  `;
15
15
  export class CLILogger {
16
16
  silent;
@@ -7,7 +7,7 @@ export const pikkuFunctionTypes = async (logger, { typesDeclarationFile: typesFi
7
7
  sessionServiceType: true,
8
8
  singletonServicesType: true,
9
9
  });
10
- const content = serializePikkuTypes(`import type { ${userSessionType.type} } from '${getFileImportRelativePath(typesFile, userSessionType.typePath, packageMappings)}'`, userSessionType.type, `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`, singletonServicesType.type, `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`, `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`);
10
+ const content = serializePikkuTypes(`import type { ${userSessionType.type} } from '${getFileImportRelativePath(typesFile, userSessionType.typePath, packageMappings)}'`, userSessionType.type, `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`, singletonServicesType.type, `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`, sessionServicesType.type, `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`);
11
11
  await writeFileInDir(logger, typesFile, content);
12
12
  });
13
13
  };
@@ -7,7 +7,7 @@ export const pikkuFunctionTypes = async (logger, { typesDeclarationFile: typesFi
7
7
  sessionServiceType: true,
8
8
  singletonServicesType: true,
9
9
  });
10
- const content = serializePikkuTypes(`import type { ${userSessionType.type} } from '${getFileImportRelativePath(typesFile, userSessionType.typePath, packageMappings)}'`, userSessionType.type, `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`, singletonServicesType.type, `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`, `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`);
10
+ const content = serializePikkuTypes(`import type { ${userSessionType.type} } from '${getFileImportRelativePath(typesFile, userSessionType.typePath, packageMappings)}'`, userSessionType.type, `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`, singletonServicesType.type, `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`, sessionServicesType.type, `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`);
11
11
  await writeFileInDir(logger, typesFile, content);
12
12
  });
13
13
  };
@@ -75,5 +75,5 @@ export interface OpenAPISpecInfo {
75
75
  [key: string]: any[];
76
76
  }[];
77
77
  }
78
- export declare function generateOpenAPISpec(functionsMeta: FunctionsMeta, routeMeta: HTTPWiringsMeta, schemas: Record<string, any>, additionalInfo: OpenAPISpecInfo): Promise<OpenAPISpec>;
78
+ export declare function generateOpenAPISpec(functionsMeta: FunctionsMeta, httpMeta: HTTPWiringsMeta, schemas: Record<string, any>, additionalInfo: OpenAPISpecInfo): Promise<OpenAPISpec>;
79
79
  export {};
@@ -10,13 +10,18 @@ const getErrorResponseForConstructorName = (constructorName) => {
10
10
  return undefined;
11
11
  };
12
12
  const convertSchemasToBodyPayloads = async (functionsMeta, routesMeta, schemas) => {
13
- const requiredSchemas = new Set(routesMeta
14
- .map(({ inputTypes, pikkuFuncName }) => {
15
- const output = functionsMeta[pikkuFuncName]?.outputs?.[0];
16
- return [inputTypes?.body, output];
17
- })
18
- .flat()
19
- .filter((schema) => !!schema));
13
+ const requiredSchemas = new Set();
14
+ for (const routeMeta of Object.values(routesMeta)) {
15
+ for (const { inputTypes, pikkuFuncName } of Object.values(routeMeta)) {
16
+ const output = functionsMeta[pikkuFuncName]?.outputs?.[0];
17
+ if (inputTypes?.body) {
18
+ requiredSchemas.add(inputTypes?.body);
19
+ }
20
+ if (output) {
21
+ requiredSchemas.add(output);
22
+ }
23
+ }
24
+ }
20
25
  const convertedEntries = await Promise.all(Object.entries(schemas).map(async ([key, schema]) => {
21
26
  if (requiredSchemas.has(key)) {
22
27
  const convertedSchema = await convertSchema(schema, {
@@ -29,90 +34,92 @@ const convertSchemasToBodyPayloads = async (functionsMeta, routesMeta, schemas)
29
34
  }));
30
35
  return Object.fromEntries(convertedEntries.filter((s) => !!s));
31
36
  };
32
- export async function generateOpenAPISpec(functionsMeta, routeMeta, schemas, additionalInfo) {
37
+ export async function generateOpenAPISpec(functionsMeta, httpMeta, schemas, additionalInfo) {
33
38
  const paths = {};
34
- routeMeta.forEach((meta) => {
35
- const { route, method, inputTypes, pikkuFuncName, params, query, docs } = meta;
36
- const functionMeta = functionsMeta[pikkuFuncName];
37
- if (!functionMeta) {
38
- console.error(`• No function metadata found for '${pikkuFuncName}' in route '${route}'.`);
39
- return;
40
- }
41
- const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined;
42
- const path = route.replace(/:(\w+)/g, '{$1}'); // Convert ":param" to "{param}"
43
- if (!paths[path]) {
44
- paths[path] = {};
45
- }
46
- const responses = {};
47
- docs?.errors?.forEach((error) => {
48
- const errorResponse = getErrorResponseForConstructorName(error);
49
- if (errorResponse) {
50
- responses[errorResponse.status] = {
51
- description: errorResponse.message,
52
- };
39
+ for (const routeMeta of Object.values(httpMeta)) {
40
+ for (const meta of Object.values(routeMeta)) {
41
+ const { route, method, inputTypes, pikkuFuncName, params, query, docs } = meta;
42
+ const functionMeta = functionsMeta[pikkuFuncName];
43
+ if (!functionMeta) {
44
+ console.error(`• No function metadata found for '${pikkuFuncName}' in route '${route}'.`);
45
+ continue;
53
46
  }
54
- });
55
- const operation = {
56
- description: docs?.description ||
57
- `This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
58
- tags: docs?.tags || [route.split('/')[1] || 'default'],
59
- parameters: [],
60
- responses: {
61
- ...responses,
62
- '200': {
63
- description: 'Successful response',
64
- content: output
65
- ? {
66
- 'application/json': {
67
- schema: typeof output === 'string' &&
68
- ['boolean', 'string', 'number'].includes(output)
69
- ? { type: output }
70
- : { $ref: `#/components/schemas/${output}` },
71
- },
72
- }
73
- : undefined,
74
- },
75
- },
76
- };
77
- const bodyType = inputTypes?.body;
78
- if (bodyType) {
79
- operation.requestBody = {
80
- required: true,
81
- content: {
82
- 'application/json': {
83
- schema: typeof bodyType === 'string' &&
84
- ['boolean', 'string', 'number'].includes(bodyType)
85
- ? { type: bodyType }
86
- : { $ref: `#/components/schemas/${bodyType}` },
47
+ const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined;
48
+ const path = route.replace(/:(\w+)/g, '{$1}'); // Convert ":param" to "{param}"
49
+ if (!paths[path]) {
50
+ paths[path] = {};
51
+ }
52
+ const responses = {};
53
+ docs?.errors?.forEach((error) => {
54
+ const errorResponse = getErrorResponseForConstructorName(error);
55
+ if (errorResponse) {
56
+ responses[errorResponse.status] = {
57
+ description: errorResponse.message,
58
+ };
59
+ }
60
+ });
61
+ const operation = {
62
+ description: docs?.description ||
63
+ `This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
64
+ tags: docs?.tags || [route.split('/')[1] || 'default'],
65
+ parameters: [],
66
+ responses: {
67
+ ...responses,
68
+ '200': {
69
+ description: 'Successful response',
70
+ content: output
71
+ ? {
72
+ 'application/json': {
73
+ schema: typeof output === 'string' &&
74
+ ['boolean', 'string', 'number'].includes(output)
75
+ ? { type: output }
76
+ : { $ref: `#/components/schemas/${output}` },
77
+ },
78
+ }
79
+ : undefined,
87
80
  },
88
81
  },
89
82
  };
83
+ const bodyType = inputTypes?.body;
84
+ if (bodyType) {
85
+ operation.requestBody = {
86
+ required: true,
87
+ content: {
88
+ 'application/json': {
89
+ schema: typeof bodyType === 'string' &&
90
+ ['boolean', 'string', 'number'].includes(bodyType)
91
+ ? { type: bodyType }
92
+ : { $ref: `#/components/schemas/${bodyType}` },
93
+ },
94
+ },
95
+ };
96
+ }
97
+ if (params) {
98
+ operation.parameters = params.map((param) => ({
99
+ name: param,
100
+ in: 'path',
101
+ required: true,
102
+ schema: { type: 'string' },
103
+ }));
104
+ }
105
+ if (query) {
106
+ operation.parameters.push(...query.map((query) => ({
107
+ name: query,
108
+ in: 'query',
109
+ required: false,
110
+ schema: { type: 'string' },
111
+ })));
112
+ }
113
+ paths[path][method] = operation;
90
114
  }
91
- if (params) {
92
- operation.parameters = params.map((param) => ({
93
- name: param,
94
- in: 'path',
95
- required: true,
96
- schema: { type: 'string' },
97
- }));
98
- }
99
- if (query) {
100
- operation.parameters.push(...query.map((query) => ({
101
- name: query,
102
- in: 'query',
103
- required: false,
104
- schema: { type: 'string' },
105
- })));
106
- }
107
- paths[path][method] = operation;
108
- });
115
+ }
109
116
  return {
110
117
  openapi: '3.1.0',
111
118
  info: additionalInfo.info,
112
119
  servers: additionalInfo.servers,
113
120
  paths,
114
121
  components: {
115
- schemas: await convertSchemasToBodyPayloads(functionsMeta, routeMeta, schemas),
122
+ schemas: await convertSchemasToBodyPayloads(functionsMeta, httpMeta, schemas),
116
123
  responses: {},
117
124
  parameters: {},
118
125
  examples: {},
@@ -34,28 +34,32 @@ export type HTTPWiringsWithMethod<Method extends string> = {
34
34
  function generateHTTPWirings(routesMeta, functionsMeta, typesMap, requiredTypes) {
35
35
  // Initialize an object to collect routes
36
36
  const routesObj = {};
37
- for (const meta of routesMeta) {
38
- const { route, method, pikkuFuncName } = meta;
39
- const functionMeta = functionsMeta[pikkuFuncName];
40
- if (!functionMeta) {
41
- throw new Error(`Function ${pikkuFuncName} not found in functionsMeta. Please check your configuration.`);
37
+ for (const methods of Object.values(routesMeta)) {
38
+ for (const meta of Object.values(methods)) {
39
+ const { route, method, pikkuFuncName } = meta;
40
+ const functionMeta = functionsMeta[pikkuFuncName];
41
+ if (!functionMeta) {
42
+ throw new Error(`Function ${pikkuFuncName} not found in functionsMeta. Please check your configuration.`);
43
+ }
44
+ const input = functionMeta.inputs ? functionMeta.inputs[0] : undefined;
45
+ const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined;
46
+ // Initialize the route entry if it doesn't exist
47
+ if (!routesObj[route]) {
48
+ routesObj[route] = {};
49
+ }
50
+ // Store the input and output types separately for RouteHandler
51
+ const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null';
52
+ const outputType = output
53
+ ? typesMap.getTypeMeta(output).uniqueName
54
+ : 'null';
55
+ requiredTypes.add(inputType);
56
+ requiredTypes.add(outputType);
57
+ // Add method entry
58
+ routesObj[route][method] = {
59
+ inputType,
60
+ outputType,
61
+ };
42
62
  }
43
- const input = functionMeta.inputs ? functionMeta.inputs[0] : undefined;
44
- const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined;
45
- // Initialize the route entry if it doesn't exist
46
- if (!routesObj[route]) {
47
- routesObj[route] = {};
48
- }
49
- // Store the input and output types separately for RouteHandler
50
- const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null';
51
- const outputType = output ? typesMap.getTypeMeta(output).uniqueName : 'null';
52
- requiredTypes.add(inputType);
53
- requiredTypes.add(outputType);
54
- // Add method entry
55
- routesObj[route][method] = {
56
- inputType,
57
- outputType,
58
- };
59
63
  }
60
64
  // Build the routes object as a string
61
65
  let routesStr = 'export type HTTPWiringsMap = {\n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.0.2",
25
- "@pikku/core": "^0.9.5",
26
- "@pikku/inspector": "^0.9.3",
25
+ "@pikku/core": "^0.9.6",
26
+ "@pikku/inspector": "^0.9.4",
27
27
  "@types/cookie": "^1.0.0",
28
28
  "@types/uuid": "^10.0.0",
29
29
  "chalk": "^5.5.0",
@@ -22,15 +22,18 @@ export async function generateSchemas(
22
22
  }
23
23
  }
24
24
  }
25
- for (const { inputTypes } of httpWiringsMeta) {
26
- if (inputTypes?.body) {
27
- schemasSet.add(inputTypes.body)
28
- }
29
- if (inputTypes?.query) {
30
- schemasSet.add(inputTypes.query)
31
- }
32
- if (inputTypes?.params) {
33
- schemasSet.add(inputTypes.params)
25
+
26
+ for (const wiringRoutes of Object.values(httpWiringsMeta)) {
27
+ for (const { inputTypes } of Object.values(wiringRoutes)) {
28
+ if (inputTypes?.body) {
29
+ schemasSet.add(inputTypes.body)
30
+ }
31
+ if (inputTypes?.query) {
32
+ schemasSet.add(inputTypes.query)
33
+ }
34
+ if (inputTypes?.params) {
35
+ schemasSet.add(inputTypes.params)
36
+ }
34
37
  }
35
38
  }
36
39
 
@@ -7,6 +7,7 @@ export const serializePikkuTypes = (
7
7
  singletonServicesTypeImport: string,
8
8
  singletonServicesTypeName: string,
9
9
  sessionServicesTypeImport: string,
10
+ sessionServicesTypeName: string,
10
11
  rpcMapTypeImport: string
11
12
  ) => {
12
13
  return `/**
@@ -26,6 +27,10 @@ ${singletonServicesTypeImport}
26
27
  ${sessionServicesTypeImport}
27
28
  ${rpcMapTypeImport}
28
29
 
30
+ ${singletonServicesTypeName !== 'SingletonServices' ? `type SingletonServices = ${singletonServicesTypeName}` : ''}
31
+ ${sessionServicesTypeName !== 'Services' ? `type Services = ${sessionServicesTypeName}` : ''}
32
+ ${userSessionTypeName !== 'Session' ? `type Session = ${userSessionTypeName}` : ''}
33
+
29
34
  /**
30
35
  * Type-safe API permission definition that integrates with your application's session type.
31
36
  * Use this to define authorization logic for your API endpoints.
@@ -33,7 +38,7 @@ ${rpcMapTypeImport}
33
38
  * @template In - The input type that the permission check will receive
34
39
  * @template RequiredServices - The services required for this permission check
35
40
  */
36
- export type PikkuPermission<In = unknown, RequiredServices extends ${singletonServicesTypeName} = ${singletonServicesTypeName}> = CorePikkuPermission<In, RequiredServices, ${userSessionTypeName}>
41
+ type PikkuPermission<In = unknown, RequiredServices extends Services = Services> = CorePikkuPermission<In, RequiredServices, Session>
37
42
 
38
43
  /**
39
44
  * Type-safe middleware definition that can access your application's services and session.
@@ -41,7 +46,38 @@ export type PikkuPermission<In = unknown, RequiredServices extends ${singletonSe
41
46
  *
42
47
  * @template RequiredServices - The services required for this middleware
43
48
  */
44
- export type PikkuMiddleware<RequiredServices extends ${singletonServicesTypeName} = ${singletonServicesTypeName}> = CorePikkuMiddleware<RequiredServices, ${userSessionTypeName}>
49
+ type PikkuMiddleware<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuMiddleware<RequiredServices, Session>
50
+
51
+ /**
52
+ * Factory function for creating permissions with tree-shaking support.
53
+ * This enables the bundler to detect which services your permission actually uses.
54
+ *
55
+ * @example
56
+ * \`\`\`typescript
57
+ * const permission = pikkuPermission(({ logger }, data, session) => {
58
+ * return session?.isAdmin || false
59
+ * })
60
+ * \`\`\`
61
+ */
62
+ export const pikkuPermission = <In>(func: PikkuPermission<In>) => {
63
+ return func
64
+ }
65
+
66
+ /**
67
+ * Factory function for creating middleware with tree-shaking support.
68
+ * This enables the bundler to detect which services your middleware actually uses.
69
+ *
70
+ * @example
71
+ * \`\`\`typescript
72
+ * const middleware = pikkuMiddleware(({ logger }, interactions, next) => {
73
+ * logger.info('Middleware executed')
74
+ * await next()
75
+ * })
76
+ * \`\`\`
77
+ */
78
+ export const pikkuMiddleware = (func: PikkuMiddleware) => {
79
+ return func
80
+ }
45
81
 
46
82
  /**
47
83
  * A sessionless API function that doesn't require user authentication.
@@ -67,7 +103,7 @@ type PikkuFunctionSessionless<
67
103
  ? { mcp?: PikkuMCP } // Optional MCP
68
104
  : { mcp: PikkuMCP } // Required MCP
69
105
  )
70
- > = CorePikkuFunctionSessionless<In, Out, ChannelData, RequiredServices, ${userSessionTypeName}>
106
+ > = CorePikkuFunctionSessionless<In, Out, ChannelData, RequiredServices, Session>
71
107
 
72
108
  /**
73
109
  * A session-aware API function that requires user authentication.
@@ -93,7 +129,7 @@ type PikkuFunction<
93
129
  ? { mcp?: PikkuMCP } // Optional MCP
94
130
  : { mcp: PikkuMCP } // Required MCP
95
131
  )
96
- > = CorePikkuFunction<In, Out, ChannelData, RequiredServices, ${userSessionTypeName}>
132
+ > = CorePikkuFunction<In, Out, ChannelData, RequiredServices, Session>
97
133
 
98
134
  /**
99
135
  * Type definition for HTTP API wirings with type-safe path parameters.
@@ -523,7 +559,7 @@ export const pikkuMCPPromptFunc = <In>(
523
559
  * const results = await fileSystem.search(input.query, input.directory)
524
560
  * return [{
525
561
  * type: 'text',
526
- * text: \`Found \${results.length} files matching \"\${input.query}\"\`
562
+ * text: \`Found \${results.length} files matching "\${input.query}"\`
527
563
  * }]
528
564
  * }
529
565
  * })
package/src/utils.ts CHANGED
@@ -13,7 +13,7 @@ const logo = `
13
13
  _____) )| | _| | _ _ _
14
14
  | ____/ | |_/ ) |_/ ) | | |
15
15
  | | | | _ (| _ (| |_| |
16
- |_| |_|_| \_)_| \_)____/
16
+ |_| |_|_| _)_| _)____/
17
17
  `
18
18
 
19
19
  export class CLILogger {
@@ -43,6 +43,7 @@ export const pikkuFunctionTypes: PikkuCommand = async (
43
43
  `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`,
44
44
  singletonServicesType.type,
45
45
  `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`,
46
+ sessionServicesType.type,
46
47
  `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`
47
48
  )
48
49
 
@@ -43,6 +43,7 @@ export const pikkuFunctionTypes: PikkuCommand = async (
43
43
  `import type { ${singletonServicesType.type} } from '${getFileImportRelativePath(typesFile, singletonServicesType.typePath, packageMappings)}'`,
44
44
  singletonServicesType.type,
45
45
  `import type { ${sessionServicesType.type} } from '${getFileImportRelativePath(typesFile, sessionServicesType.typePath, packageMappings)}'`,
46
+ sessionServicesType.type,
46
47
  `import type { TypedPikkuRPC } from '${getFileImportRelativePath(typesFile, rpcInternalMapDeclarationFile, packageMappings)}'`
47
48
  )
48
49
  await writeFileInDir(logger, typesFile, content)
@@ -79,15 +79,19 @@ const convertSchemasToBodyPayloads = async (
79
79
  routesMeta: HTTPWiringsMeta,
80
80
  schemas: Record<string, any>
81
81
  ) => {
82
- const requiredSchemas = new Set(
83
- routesMeta
84
- .map(({ inputTypes, pikkuFuncName }) => {
85
- const output = functionsMeta[pikkuFuncName]?.outputs?.[0]
86
- return [inputTypes?.body, output]
87
- })
88
- .flat()
89
- .filter((schema) => !!schema)
90
- )
82
+ const requiredSchemas = new Set<string>()
83
+ for (const routeMeta of Object.values(routesMeta)) {
84
+ for (const { inputTypes, pikkuFuncName } of Object.values(routeMeta)) {
85
+ const output = functionsMeta[pikkuFuncName]?.outputs?.[0]
86
+ if (inputTypes?.body) {
87
+ requiredSchemas.add(inputTypes?.body)
88
+ }
89
+ if (output) {
90
+ requiredSchemas.add(output)
91
+ }
92
+ }
93
+ }
94
+
91
95
  const convertedEntries = await Promise.all(
92
96
  Object.entries(schemas).map(async ([key, schema]) => {
93
97
  if (requiredSchemas.has(key)) {
@@ -105,103 +109,106 @@ const convertSchemasToBodyPayloads = async (
105
109
 
106
110
  export async function generateOpenAPISpec(
107
111
  functionsMeta: FunctionsMeta,
108
- routeMeta: HTTPWiringsMeta,
112
+ httpMeta: HTTPWiringsMeta,
109
113
  schemas: Record<string, any>,
110
114
  additionalInfo: OpenAPISpecInfo
111
115
  ): Promise<OpenAPISpec> {
112
116
  const paths: Record<string, any> = {}
113
117
 
114
- routeMeta.forEach((meta) => {
115
- const { route, method, inputTypes, pikkuFuncName, params, query, docs } =
116
- meta
117
- const functionMeta = functionsMeta[pikkuFuncName]
118
- if (!functionMeta) {
119
- console.error(
120
- `• No function metadata found for '${pikkuFuncName}' in route '${route}'.`
121
- )
122
- return
123
- }
124
- const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined
118
+ for (const routeMeta of Object.values(httpMeta)) {
119
+ for (const meta of Object.values(routeMeta)) {
120
+ const { route, method, inputTypes, pikkuFuncName, params, query, docs } =
121
+ meta
122
+ const functionMeta = functionsMeta[pikkuFuncName]
123
+ if (!functionMeta) {
124
+ console.error(
125
+ `• No function metadata found for '${pikkuFuncName}' in route '${route}'.`
126
+ )
127
+ continue
128
+ }
125
129
 
126
- const path = route.replace(/:(\w+)/g, '{$1}') // Convert ":param" to "{param}"
130
+ const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined
127
131
 
128
- if (!paths[path]) {
129
- paths[path] = {}
130
- }
132
+ const path = route.replace(/:(\w+)/g, '{$1}') // Convert ":param" to "{param}"
131
133
 
132
- const responses = {}
133
- docs?.errors?.forEach((error) => {
134
- const errorResponse = getErrorResponseForConstructorName(error)
135
- if (errorResponse) {
136
- responses[errorResponse.status] = {
137
- description: errorResponse.message,
138
- }
134
+ if (!paths[path]) {
135
+ paths[path] = {}
139
136
  }
140
- })
141
137
 
142
- const operation: any = {
143
- description:
144
- docs?.description ||
145
- `This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
146
- tags: docs?.tags || [route.split('/')[1] || 'default'],
147
- parameters: [],
148
- responses: {
149
- ...responses,
150
- '200': {
151
- description: 'Successful response',
152
- content: output
153
- ? {
154
- 'application/json': {
155
- schema:
156
- typeof output === 'string' &&
157
- ['boolean', 'string', 'number'].includes(output)
158
- ? { type: output }
159
- : { $ref: `#/components/schemas/${output}` },
160
- },
161
- }
162
- : undefined,
163
- },
164
- },
165
- }
138
+ const responses = {}
139
+ docs?.errors?.forEach((error) => {
140
+ const errorResponse = getErrorResponseForConstructorName(error)
141
+ if (errorResponse) {
142
+ responses[errorResponse.status] = {
143
+ description: errorResponse.message,
144
+ }
145
+ }
146
+ })
166
147
 
167
- const bodyType = inputTypes?.body
168
- if (bodyType) {
169
- operation.requestBody = {
170
- required: true,
171
- content: {
172
- 'application/json': {
173
- schema:
174
- typeof bodyType === 'string' &&
175
- ['boolean', 'string', 'number'].includes(bodyType)
176
- ? { type: bodyType }
177
- : { $ref: `#/components/schemas/${bodyType}` },
148
+ const operation: any = {
149
+ description:
150
+ docs?.description ||
151
+ `This endpoint handles the ${method.toUpperCase()} request for the route ${route}.`,
152
+ tags: docs?.tags || [route.split('/')[1] || 'default'],
153
+ parameters: [],
154
+ responses: {
155
+ ...responses,
156
+ '200': {
157
+ description: 'Successful response',
158
+ content: output
159
+ ? {
160
+ 'application/json': {
161
+ schema:
162
+ typeof output === 'string' &&
163
+ ['boolean', 'string', 'number'].includes(output)
164
+ ? { type: output }
165
+ : { $ref: `#/components/schemas/${output}` },
166
+ },
167
+ }
168
+ : undefined,
178
169
  },
179
170
  },
180
171
  }
181
- }
182
172
 
183
- if (params) {
184
- operation.parameters = params.map((param) => ({
185
- name: param,
186
- in: 'path',
187
- required: true,
188
- schema: { type: 'string' },
189
- }))
190
- }
173
+ const bodyType = inputTypes?.body
174
+ if (bodyType) {
175
+ operation.requestBody = {
176
+ required: true,
177
+ content: {
178
+ 'application/json': {
179
+ schema:
180
+ typeof bodyType === 'string' &&
181
+ ['boolean', 'string', 'number'].includes(bodyType)
182
+ ? { type: bodyType }
183
+ : { $ref: `#/components/schemas/${bodyType}` },
184
+ },
185
+ },
186
+ }
187
+ }
191
188
 
192
- if (query) {
193
- operation.parameters.push(
194
- ...query.map((query) => ({
195
- name: query,
196
- in: 'query',
197
- required: false,
189
+ if (params) {
190
+ operation.parameters = params.map((param) => ({
191
+ name: param,
192
+ in: 'path',
193
+ required: true,
198
194
  schema: { type: 'string' },
199
195
  }))
200
- )
201
- }
196
+ }
202
197
 
203
- paths[path][method] = operation
204
- })
198
+ if (query) {
199
+ operation.parameters.push(
200
+ ...query.map((query) => ({
201
+ name: query,
202
+ in: 'query',
203
+ required: false,
204
+ schema: { type: 'string' },
205
+ }))
206
+ )
207
+ }
208
+
209
+ paths[path][method] = operation
210
+ }
211
+ }
205
212
 
206
213
  return {
207
214
  openapi: '3.1.0',
@@ -211,7 +218,7 @@ export async function generateOpenAPISpec(
211
218
  components: {
212
219
  schemas: await convertSchemasToBodyPayloads(
213
220
  functionsMeta,
214
- routeMeta,
221
+ httpMeta,
215
222
  schemas
216
223
  ),
217
224
  responses: {},
@@ -67,33 +67,37 @@ function generateHTTPWirings(
67
67
  Record<string, { inputType: string; outputType: string }>
68
68
  > = {}
69
69
 
70
- for (const meta of routesMeta) {
71
- const { route, method, pikkuFuncName } = meta
72
- const functionMeta = functionsMeta[pikkuFuncName]
73
- if (!functionMeta) {
74
- throw new Error(
75
- `Function ${pikkuFuncName} not found in functionsMeta. Please check your configuration.`
76
- )
77
- }
78
- const input = functionMeta.inputs ? functionMeta.inputs[0] : undefined
79
- const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined
80
-
81
- // Initialize the route entry if it doesn't exist
82
- if (!routesObj[route]) {
83
- routesObj[route] = {}
84
- }
85
-
86
- // Store the input and output types separately for RouteHandler
87
- const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null'
88
- const outputType = output ? typesMap.getTypeMeta(output).uniqueName : 'null'
89
-
90
- requiredTypes.add(inputType)
91
- requiredTypes.add(outputType)
92
-
93
- // Add method entry
94
- routesObj[route][method] = {
95
- inputType,
96
- outputType,
70
+ for (const methods of Object.values(routesMeta)) {
71
+ for (const meta of Object.values(methods)) {
72
+ const { route, method, pikkuFuncName } = meta
73
+ const functionMeta = functionsMeta[pikkuFuncName]
74
+ if (!functionMeta) {
75
+ throw new Error(
76
+ `Function ${pikkuFuncName} not found in functionsMeta. Please check your configuration.`
77
+ )
78
+ }
79
+ const input = functionMeta.inputs ? functionMeta.inputs[0] : undefined
80
+ const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined
81
+
82
+ // Initialize the route entry if it doesn't exist
83
+ if (!routesObj[route]) {
84
+ routesObj[route] = {}
85
+ }
86
+
87
+ // Store the input and output types separately for RouteHandler
88
+ const inputType = input ? typesMap.getTypeMeta(input).uniqueName : 'null'
89
+ const outputType = output
90
+ ? typesMap.getTypeMeta(output).uniqueName
91
+ : 'null'
92
+
93
+ requiredTypes.add(inputType)
94
+ requiredTypes.add(outputType)
95
+
96
+ // Add method entry
97
+ routesObj[route][method] = {
98
+ inputType,
99
+ outputType,
100
+ }
97
101
  }
98
102
  }
99
103