services-as-software 2.0.2 → 2.1.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/src/service.js ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Service implementation
3
+ */
4
+ /**
5
+ * Create a service from a definition
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const service = Service({
10
+ * name: 'translation-service',
11
+ * version: '1.0.0',
12
+ * description: 'AI-powered translation service',
13
+ * pricing: {
14
+ * model: 'per-use',
15
+ * pricePerUnit: 0.01,
16
+ * currency: 'USD',
17
+ * },
18
+ * endpoints: [
19
+ * Endpoint({
20
+ * name: 'translate',
21
+ * method: 'POST',
22
+ * path: '/translate',
23
+ * input: {
24
+ * type: 'object',
25
+ * properties: {
26
+ * text: { type: 'string' },
27
+ * from: { type: 'string' },
28
+ * to: { type: 'string' },
29
+ * },
30
+ * required: ['text', 'to'],
31
+ * },
32
+ * handler: async (input) => {
33
+ * // Translation logic here
34
+ * return { translatedText: input.text }
35
+ * },
36
+ * }),
37
+ * ],
38
+ * })
39
+ * ```
40
+ */
41
+ export function Service(definition) {
42
+ // Store endpoints by name for quick lookup
43
+ const endpointMap = new Map();
44
+ for (const endpoint of definition.endpoints) {
45
+ endpointMap.set(endpoint.name, endpoint);
46
+ }
47
+ // Store event handlers
48
+ const eventHandlers = new Map();
49
+ if (definition.events) {
50
+ for (const [event, handler] of Object.entries(definition.events)) {
51
+ eventHandlers.set(event, [handler]);
52
+ }
53
+ }
54
+ // Store scheduled tasks
55
+ const scheduledTasks = new Map();
56
+ if (definition.scheduled) {
57
+ for (const task of definition.scheduled) {
58
+ scheduledTasks.set(task.name, task);
59
+ }
60
+ }
61
+ // Create the service instance
62
+ const service = {
63
+ definition,
64
+ // Call an endpoint
65
+ async call(endpoint, input, context) {
66
+ const ep = endpointMap.get(endpoint);
67
+ if (!ep) {
68
+ throw new Error(`Endpoint not found: ${endpoint}`);
69
+ }
70
+ // Create context if not provided
71
+ const ctx = context || {
72
+ requestId: generateRequestId(),
73
+ entitlements: [],
74
+ };
75
+ // Track usage if tracker is available
76
+ if (ctx.usage && ctx.customerId) {
77
+ await ctx.usage.track({
78
+ customerId: ctx.customerId,
79
+ resource: endpoint,
80
+ quantity: 1,
81
+ timestamp: new Date(),
82
+ });
83
+ }
84
+ // Call the handler
85
+ return ep.handler(input, ctx);
86
+ },
87
+ // Ask a question
88
+ async ask(question, context) {
89
+ return service.call('ask', { question, context });
90
+ },
91
+ // Deliver results
92
+ async deliver(orderId, results) {
93
+ return service.call('deliver', { orderId, results });
94
+ },
95
+ // Execute a task
96
+ async do(action, input) {
97
+ return service.call('do', { action, input });
98
+ },
99
+ // Generate content
100
+ async generate(prompt, options) {
101
+ return service.call('generate', { prompt, options });
102
+ },
103
+ // Type checking/validation
104
+ async is(value, type) {
105
+ return service.call('is', { value, type });
106
+ },
107
+ // Send notification
108
+ async notify(notification) {
109
+ return service.call('notify', notification);
110
+ },
111
+ // Place an order
112
+ async order(product, quantity) {
113
+ return service.call('order', { product, quantity });
114
+ },
115
+ // Request a quote
116
+ async quote(product, quantity) {
117
+ return service.call('quote', { product, quantity });
118
+ },
119
+ // Subscribe to a plan
120
+ async subscribe(planId) {
121
+ return service.call('subscribe', { planId });
122
+ },
123
+ // Get entitlements
124
+ async entitlements() {
125
+ return service.call('entitlements', {});
126
+ },
127
+ // Get KPIs
128
+ async kpis() {
129
+ const result = {};
130
+ if (definition.kpis) {
131
+ for (const kpi of definition.kpis) {
132
+ result[kpi.id] = await kpi.calculate();
133
+ }
134
+ }
135
+ return result;
136
+ },
137
+ // Get OKRs
138
+ async okrs() {
139
+ if (!definition.okrs) {
140
+ return [];
141
+ }
142
+ // Calculate current values for key results
143
+ const okrs = await Promise.all(definition.okrs.map(async (okr) => {
144
+ const keyResults = await Promise.all(okr.keyResults.map(async (kr) => ({
145
+ ...kr,
146
+ current: await kr.measure(),
147
+ })));
148
+ return { ...okr, keyResults };
149
+ }));
150
+ return okrs;
151
+ },
152
+ // Register event handler
153
+ on(event, handler) {
154
+ const handlers = eventHandlers.get(event) || [];
155
+ handlers.push({
156
+ event,
157
+ handler: handler,
158
+ });
159
+ eventHandlers.set(event, handlers);
160
+ },
161
+ // Schedule recurring task
162
+ every(schedule, handler) {
163
+ const taskName = `task-${scheduledTasks.size + 1}`;
164
+ scheduledTasks.set(taskName, {
165
+ name: taskName,
166
+ schedule,
167
+ handler: (_input, context) => handler(context),
168
+ enabled: true,
169
+ });
170
+ },
171
+ // Add queue processor
172
+ queue(name, handler) {
173
+ // Queue processing would typically integrate with a queue system
174
+ // For now, we add it as an event handler
175
+ service.on(`queue:${name}`, handler);
176
+ },
177
+ // Get service as RPC target
178
+ asRPC() {
179
+ // This would integrate with the RPC system from ai-functions
180
+ // For now, return a placeholder
181
+ return {
182
+ _type: 'rpc-target',
183
+ service: definition.name,
184
+ version: definition.version,
185
+ };
186
+ },
187
+ // Get service as API routes
188
+ asAPI() {
189
+ // This would generate HTTP/REST API routes
190
+ // For now, return a placeholder with route definitions
191
+ return definition.endpoints.map((ep) => ({
192
+ method: ep.method || 'POST',
193
+ path: ep.path || `/${ep.name}`,
194
+ handler: ep.handler,
195
+ }));
196
+ },
197
+ };
198
+ return service;
199
+ }
200
+ /**
201
+ * Generate a unique request ID
202
+ */
203
+ function generateRequestId() {
204
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
205
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Tests for Service implementation
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { Service, Endpoint, POST, GET } from './index.js';
6
+ describe('Service', () => {
7
+ it('should create a service with basic configuration', () => {
8
+ const service = Service({
9
+ name: 'test-service',
10
+ version: '1.0.0',
11
+ endpoints: [],
12
+ });
13
+ expect(service).toBeDefined();
14
+ expect(service.definition.name).toBe('test-service');
15
+ expect(service.definition.version).toBe('1.0.0');
16
+ });
17
+ it('should call an endpoint and return result', async () => {
18
+ const service = Service({
19
+ name: 'test-service',
20
+ version: '1.0.0',
21
+ endpoints: [
22
+ POST({
23
+ name: 'echo',
24
+ handler: async (input) => {
25
+ return { echoed: input.message };
26
+ },
27
+ }),
28
+ ],
29
+ });
30
+ const result = await service.call('echo', {
31
+ message: 'hello',
32
+ });
33
+ expect(result.echoed).toBe('hello');
34
+ });
35
+ it('should provide service context to handlers', async () => {
36
+ let capturedContext;
37
+ const service = Service({
38
+ name: 'test-service',
39
+ version: '1.0.0',
40
+ endpoints: [
41
+ POST({
42
+ name: 'test',
43
+ handler: async (_input, context) => {
44
+ capturedContext = context;
45
+ return { ok: true };
46
+ },
47
+ }),
48
+ ],
49
+ });
50
+ await service.call('test', {}, { requestId: 'test-123', entitlements: ['test'] });
51
+ expect(capturedContext).toBeDefined();
52
+ expect(capturedContext?.requestId).toBe('test-123');
53
+ expect(capturedContext?.entitlements).toContain('test');
54
+ });
55
+ it('should throw error for unknown endpoint', async () => {
56
+ const service = Service({
57
+ name: 'test-service',
58
+ version: '1.0.0',
59
+ endpoints: [],
60
+ });
61
+ await expect(service.call('unknown', {})).rejects.toThrow('Endpoint not found: unknown');
62
+ });
63
+ it('should calculate KPIs', async () => {
64
+ const service = Service({
65
+ name: 'test-service',
66
+ version: '1.0.0',
67
+ endpoints: [],
68
+ kpis: [
69
+ {
70
+ id: 'test-kpi',
71
+ name: 'Test KPI',
72
+ calculate: async () => 42,
73
+ },
74
+ ],
75
+ });
76
+ const kpis = await service.kpis();
77
+ expect(kpis['test-kpi']).toBe(42);
78
+ });
79
+ it('should calculate OKRs with key results', async () => {
80
+ const service = Service({
81
+ name: 'test-service',
82
+ version: '1.0.0',
83
+ endpoints: [],
84
+ okrs: [
85
+ {
86
+ id: 'okr-1',
87
+ objective: 'Test objective',
88
+ keyResults: [
89
+ {
90
+ description: 'Test key result',
91
+ measure: async () => 75,
92
+ target: 100,
93
+ unit: 'points',
94
+ },
95
+ ],
96
+ },
97
+ ],
98
+ });
99
+ const okrs = await service.okrs();
100
+ expect(okrs).toHaveLength(1);
101
+ expect(okrs[0]?.objective).toBe('Test objective');
102
+ expect(okrs[0]?.keyResults[0]?.current).toBe(75);
103
+ expect(okrs[0]?.keyResults[0]?.target).toBe(100);
104
+ });
105
+ it('should register event handlers', () => {
106
+ const service = Service({
107
+ name: 'test-service',
108
+ version: '1.0.0',
109
+ endpoints: [],
110
+ });
111
+ let eventFired = false;
112
+ service.on('test.event', () => {
113
+ eventFired = true;
114
+ });
115
+ // Event handlers are registered (actual firing would happen in event system)
116
+ expect(eventFired).toBe(false); // Not fired yet
117
+ });
118
+ it('should register scheduled tasks', () => {
119
+ const service = Service({
120
+ name: 'test-service',
121
+ version: '1.0.0',
122
+ endpoints: [],
123
+ });
124
+ let taskRan = false;
125
+ service.every('0 * * * *', () => {
126
+ taskRan = true;
127
+ });
128
+ // Task is registered (actual execution would happen in scheduler)
129
+ expect(taskRan).toBe(false); // Not run yet
130
+ });
131
+ });
132
+ describe('Endpoint helpers', () => {
133
+ it('should create POST endpoint', () => {
134
+ const endpoint = POST({
135
+ name: 'test',
136
+ handler: async () => ({ ok: true }),
137
+ });
138
+ expect(endpoint.method).toBe('POST');
139
+ expect(endpoint.name).toBe('test');
140
+ });
141
+ it('should create GET endpoint', () => {
142
+ const endpoint = GET({
143
+ name: 'test',
144
+ handler: async () => ({ ok: true }),
145
+ });
146
+ expect(endpoint.method).toBe('GET');
147
+ expect(endpoint.name).toBe('test');
148
+ });
149
+ it('should default to requiring auth', () => {
150
+ const endpoint = Endpoint({
151
+ name: 'test',
152
+ handler: async () => ({ ok: true }),
153
+ });
154
+ expect(endpoint.requiresAuth).toBe(true);
155
+ });
156
+ it('should allow disabling auth requirement', () => {
157
+ const endpoint = Endpoint({
158
+ name: 'test',
159
+ handler: async () => ({ ok: true }),
160
+ requiresAuth: false,
161
+ });
162
+ expect(endpoint.requiresAuth).toBe(false);
163
+ });
164
+ });
package/src/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Core types for services-as-software
3
+ */
4
+ export {};