@simplysm/service-server 13.0.68 → 13.0.70

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.
Files changed (49) hide show
  1. package/README.md +48 -249
  2. package/dist/auth/jwt-manager.js +2 -2
  3. package/dist/auth/jwt-manager.js.map +1 -1
  4. package/dist/core/define-service.js +2 -2
  5. package/dist/core/define-service.js.map +1 -1
  6. package/dist/core/service-executor.js +5 -5
  7. package/dist/core/service-executor.js.map +1 -1
  8. package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
  9. package/dist/legacy/v1-auto-update-handler.js +2 -2
  10. package/dist/legacy/v1-auto-update-handler.js.map +1 -1
  11. package/dist/service-server.js +11 -11
  12. package/dist/service-server.js.map +1 -1
  13. package/dist/services/auto-update-service.js +1 -1
  14. package/dist/services/auto-update-service.js.map +1 -1
  15. package/dist/services/orm-service.js +6 -6
  16. package/dist/services/orm-service.js.map +1 -1
  17. package/dist/transport/http/http-request-handler.js +1 -1
  18. package/dist/transport/http/http-request-handler.js.map +1 -1
  19. package/dist/transport/http/static-file-handler.js +3 -3
  20. package/dist/transport/http/upload-handler.js +2 -2
  21. package/dist/transport/http/upload-handler.js.map +1 -1
  22. package/dist/transport/socket/service-socket.js +2 -2
  23. package/dist/transport/socket/service-socket.js.map +1 -1
  24. package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
  25. package/dist/transport/socket/websocket-handler.js +11 -9
  26. package/dist/transport/socket/websocket-handler.js.map +1 -1
  27. package/dist/utils/config-manager.js +7 -7
  28. package/dist/utils/config-manager.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/auth/jwt-manager.ts +2 -2
  31. package/src/core/define-service.ts +2 -2
  32. package/src/core/service-executor.ts +13 -13
  33. package/src/legacy/v1-auto-update-handler.ts +8 -8
  34. package/src/service-server.ts +28 -28
  35. package/src/services/auto-update-service.ts +1 -1
  36. package/src/services/orm-service.ts +6 -6
  37. package/src/transport/http/http-request-handler.ts +5 -5
  38. package/src/transport/http/static-file-handler.ts +7 -7
  39. package/src/transport/http/upload-handler.ts +3 -3
  40. package/src/transport/socket/service-socket.ts +4 -4
  41. package/src/transport/socket/websocket-handler.ts +12 -10
  42. package/src/utils/config-manager.ts +11 -11
  43. package/tests/define-service.spec.ts +85 -0
  44. package/tests/orm-service.spec.ts +83 -0
  45. package/tests/service-executor.spec.ts +114 -0
  46. package/docs/authentication.md +0 -114
  47. package/docs/built-in-services.md +0 -100
  48. package/docs/server.md +0 -374
  49. package/docs/transport.md +0 -273
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import type { QueryDef } from "@simplysm/orm-common";
3
+
4
+ vi.mock("@simplysm/orm-node", () => ({
5
+ createDbConn: vi.fn(),
6
+ }));
7
+
8
+ import { createDbConn } from "@simplysm/orm-node";
9
+ import { OrmService } from "../src/services/orm-service";
10
+
11
+ describe("OrmService.executeDefs", () => {
12
+ let mockExecute: ReturnType<typeof vi.fn>;
13
+ let methods: any;
14
+ let connId: number;
15
+
16
+ const twoDefs: QueryDef[] = [
17
+ {
18
+ type: "createTable",
19
+ table: { database: "db", name: "A" },
20
+ columns: [{ name: "id", dataType: { type: "bigint" } }],
21
+ primaryKey: ["id"],
22
+ },
23
+ {
24
+ type: "createTable",
25
+ table: { database: "db", name: "B" },
26
+ columns: [{ name: "id", dataType: { type: "bigint" } }],
27
+ primaryKey: ["id"],
28
+ },
29
+ ];
30
+
31
+ beforeEach(async () => {
32
+ mockExecute = vi.fn((queries: string[]) => Promise.resolve(queries.map(() => [])));
33
+
34
+ const mockConn = {
35
+ config: { dialect: "postgresql" as const },
36
+ isConnected: true,
37
+ connect: vi.fn(),
38
+ close: vi.fn(),
39
+ execute: mockExecute,
40
+ executeParametrized: vi.fn(),
41
+ bulkInsert: vi.fn(),
42
+ beginTransaction: vi.fn(),
43
+ commitTransaction: vi.fn(),
44
+ rollbackTransaction: vi.fn(),
45
+ on: vi.fn(),
46
+ };
47
+
48
+ vi.mocked(createDbConn).mockResolvedValue(mockConn as any);
49
+
50
+ const ctx = {
51
+ socket: { on: vi.fn() },
52
+ getConfig: vi.fn(() =>
53
+ Promise.resolve({
54
+ test: { dialect: "postgresql", host: "localhost", database: "db" },
55
+ }),
56
+ ),
57
+ clientName: "test",
58
+ };
59
+
60
+ methods = OrmService.factory(ctx as any);
61
+ connId = await methods.connect({ configName: "test" });
62
+ mockExecute.mockClear();
63
+ });
64
+
65
+ it("executes queries individually when options is undefined", async () => {
66
+ const result = await methods.executeDefs(connId, twoDefs);
67
+
68
+ // Should pass 2 separate queries, not 1 combined string
69
+ expect(mockExecute).toHaveBeenCalledTimes(1);
70
+ expect(mockExecute.mock.calls[0][0]).toHaveLength(2);
71
+
72
+ // Should return one result per def
73
+ expect(result).toHaveLength(2);
74
+ });
75
+
76
+ it("combines queries when options is explicitly all-null", async () => {
77
+ await methods.executeDefs(connId, twoDefs, [undefined, undefined]);
78
+
79
+ // Should pass 1 combined query string
80
+ expect(mockExecute).toHaveBeenCalledTimes(1);
81
+ expect(mockExecute.mock.calls[0][0]).toHaveLength(1);
82
+ });
83
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { runServiceMethod } from "../src/core/service-executor";
3
+ import { defineService, auth } from "../src/core/define-service";
4
+
5
+ // Minimal mock server
6
+ function createMockServer(services: any[]) {
7
+ return { options: { services, auth: { jwtSecret: "test" } } } as any;
8
+ }
9
+
10
+ describe("runServiceMethod with ServiceDefinition", () => {
11
+ it("executes a basic service method", async () => {
12
+ const EchoService = defineService("Echo", (_ctx) => ({
13
+ echo: (msg: string) => `Echo: ${msg}`,
14
+ }));
15
+
16
+ const server = createMockServer([EchoService]);
17
+ const result = await runServiceMethod(server, {
18
+ serviceName: "Echo",
19
+ methodName: "echo",
20
+ params: ["hello"],
21
+ });
22
+
23
+ expect(result).toBe("Echo: hello");
24
+ });
25
+
26
+ it("throws error when service not found", async () => {
27
+ const server = createMockServer([]);
28
+
29
+ await expect(
30
+ runServiceMethod(server, { serviceName: "Unknown", methodName: "test", params: [] }),
31
+ ).rejects.toThrow("Service [Unknown] not found.");
32
+ });
33
+
34
+ it("throws error when method not found", async () => {
35
+ const svc = defineService("Test", (_ctx) => ({
36
+ existing: () => "ok",
37
+ }));
38
+ const server = createMockServer([svc]);
39
+
40
+ await expect(
41
+ runServiceMethod(server, { serviceName: "Test", methodName: "nonexistent", params: [] }),
42
+ ).rejects.toThrow("Method [Test.nonexistent] not found.");
43
+ });
44
+
45
+ it("blocks unauthenticated access to auth-required service", async () => {
46
+ const svc = defineService(
47
+ "Protected",
48
+ auth((_ctx) => ({
49
+ secret: () => "secret",
50
+ })),
51
+ );
52
+ const server = createMockServer([svc]);
53
+
54
+ await expect(
55
+ runServiceMethod(server, { serviceName: "Protected", methodName: "secret", params: [] }),
56
+ ).rejects.toThrow("Login is required.");
57
+ });
58
+
59
+ it("blocks unauthorized role access", async () => {
60
+ const svc = defineService(
61
+ "Admin",
62
+ auth((_ctx) => ({
63
+ manage: auth(["admin"], () => "managed"),
64
+ view: () => "viewed",
65
+ })),
66
+ );
67
+ const server = createMockServer([svc]);
68
+
69
+ // Has auth but wrong role
70
+ await expect(
71
+ runServiceMethod(server, {
72
+ serviceName: "Admin",
73
+ methodName: "manage",
74
+ params: [],
75
+ http: { clientName: "test", authTokenPayload: { roles: ["user"], data: {} } as any },
76
+ }),
77
+ ).rejects.toThrow("Insufficient permissions.");
78
+ });
79
+
80
+ it("allows access with correct role", async () => {
81
+ const svc = defineService(
82
+ "Admin",
83
+ auth((_ctx) => ({
84
+ manage: auth(["admin"], () => "managed"),
85
+ })),
86
+ );
87
+ const server = createMockServer([svc]);
88
+
89
+ const result = await runServiceMethod(server, {
90
+ serviceName: "Admin",
91
+ methodName: "manage",
92
+ params: [],
93
+ http: { clientName: "test", authTokenPayload: { roles: ["admin"], data: {} } as any },
94
+ });
95
+
96
+ expect(result).toBe("managed");
97
+ });
98
+
99
+ it("provides context to factory", async () => {
100
+ const svc = defineService("Ctx", (ctx) => ({
101
+ getClientName: () => ctx.clientName,
102
+ }));
103
+ const server = createMockServer([svc]);
104
+
105
+ const result = await runServiceMethod(server, {
106
+ serviceName: "Ctx",
107
+ methodName: "getClientName",
108
+ params: [],
109
+ http: { clientName: "my-app", authTokenPayload: undefined },
110
+ });
111
+
112
+ expect(result).toBe("my-app");
113
+ });
114
+ });
@@ -1,114 +0,0 @@
1
- # Authentication
2
-
3
- ## auth() Wrapper
4
-
5
- Use the `auth()` wrapper to set authentication requirements on services or methods. Only works when `ServiceServerOptions.auth` is configured.
6
-
7
- ```typescript
8
- import { defineService, auth } from "@simplysm/service-server";
9
-
10
- // Service-level auth: all methods require login
11
- export const UserService = defineService("User", auth((ctx) => ({
12
- // Login only required (inherits from service level)
13
- getProfile: async (): Promise<unknown> => {
14
- const userId = (ctx.authInfo as { userId: number; role: string })?.userId;
15
- // ...
16
- },
17
-
18
- // Method-level auth: specific role required (overrides service level)
19
- deleteUser: auth(["admin"], async (targetId: number): Promise<void> => {
20
- // Only users with admin role can call
21
- }),
22
- })));
23
-
24
- // No authentication required (no auth wrapper)
25
- export const PublicService = defineService("Public", (ctx) => ({
26
- healthCheck: async (): Promise<string> => {
27
- return "OK";
28
- },
29
- }));
30
- ```
31
-
32
- Auth behavior:
33
-
34
- | Target | `auth(factory)` | `auth(["admin"], fn)` |
35
- |-----------|----------------|-------------------------|
36
- | Service | All methods require login | All methods require admin role |
37
- | Method | Method requires login | Method requires admin role |
38
- | None | No auth required (Public) | - |
39
-
40
- Method-level `auth()` overrides service-level settings.
41
-
42
- ## getServiceAuthPermissions
43
-
44
- Read auth permissions from an `auth()`-wrapped function. Returns `undefined` if the function is not wrapped with `auth()`. Primarily used internally by `runServiceMethod`, but exported for advanced use cases.
45
-
46
- ```typescript
47
- import { defineService, auth, getServiceAuthPermissions } from "@simplysm/service-server";
48
-
49
- // For a method with auth wrapper
50
- const methodPerms = getServiceAuthPermissions(someMethod);
51
- // string[] if permissions are set, or undefined for public (no auth wrapper)
52
-
53
- // For a service definition
54
- const servicePerms = someServiceDef.authPermissions;
55
- // string[] if service-level auth is set, or undefined for public
56
- ```
57
-
58
- | Signature | Returns | Description |
59
- |-----------|---------|------|
60
- | `getServiceAuthPermissions(fn: Function)` | `string[] \| undefined` | Returns the permission array attached by `auth()`, or `undefined` if not wrapped |
61
-
62
- ## JWT Functions
63
-
64
- Standalone functions for JWT token generation and verification using the `jose` library (HS256 algorithm, 12-hour expiration).
65
-
66
- ```typescript
67
- import { signJwt, verifyJwt, decodeJwt } from "@simplysm/service-server";
68
-
69
- // Generate token (12-hour expiration, HS256 algorithm)
70
- const token = await signJwt("my-secret-key", {
71
- roles: ["admin", "user"],
72
- data: { userId: 1, name: "John" },
73
- });
74
-
75
- // Verify token (throws on expiry or invalid signature)
76
- const payload = await verifyJwt("my-secret-key", token);
77
- // payload.roles: ["admin", "user"]
78
- // payload.data: { userId: 1, name: "John" }
79
-
80
- // Decode token without verification (synchronous, no secret needed)
81
- const decoded = decodeJwt(token);
82
- ```
83
-
84
- | Function | Returns | Description |
85
- |----------|---------|------|
86
- | `signJwt<TAuthInfo>(jwtSecret, payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
87
- | `verifyJwt<TAuthInfo>(jwtSecret, token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify token signature and expiration, return payload. Throws on invalid or expired tokens |
88
- | `decodeJwt<TAuthInfo>(token)` | `AuthTokenPayload<TAuthInfo>` | Decode token without verification (synchronous) |
89
-
90
- In most cases, use `ServiceServer` methods instead of calling these functions directly:
91
-
92
- ```typescript
93
- // Generate token via server (preferred)
94
- const token = await server.generateAuthToken({
95
- roles: ["admin"],
96
- data: { userId: 1 },
97
- });
98
-
99
- // Verify token via server (preferred)
100
- const payload = await server.verifyAuthToken(token);
101
- ```
102
-
103
- ## AuthTokenPayload
104
-
105
- ```typescript
106
- import type { AuthTokenPayload } from "@simplysm/service-server";
107
-
108
- interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
109
- /** User role list (used for permission check in auth wrapper) */
110
- roles: string[];
111
- /** Custom auth info (generic type) */
112
- data: TAuthInfo;
113
- }
114
- ```
@@ -1,100 +0,0 @@
1
- # Built-in Services
2
-
3
- ## OrmService
4
-
5
- Provides database connection/query/transaction via WebSocket. The `auth()` wrapper is applied, requiring login.
6
-
7
- ```typescript
8
- import { createServiceServer, OrmService } from "@simplysm/service-server";
9
-
10
- const server = createServiceServer({
11
- port: 8080,
12
- rootPath: "/app/data",
13
- auth: { jwtSecret: "secret" },
14
- services: [OrmService],
15
- });
16
- ```
17
-
18
- Define ORM config in `.config.json`:
19
-
20
- ```json
21
- {
22
- "orm": {
23
- "default": {
24
- "dialect": "mysql",
25
- "host": "localhost",
26
- "port": 3306,
27
- "database": "mydb",
28
- "user": "root",
29
- "password": "password"
30
- }
31
- }
32
- }
33
- ```
34
-
35
- Methods provided by `OrmService`:
36
-
37
- | Method | Returns | Description |
38
- |--------|---------|------|
39
- | `getInfo(opt)` | `Promise<{ dialect, database?, schema? }>` | Query DB connection info |
40
- | `connect(opt)` | `Promise<number>` | Create DB connection. Returns connection ID |
41
- | `close(connId)` | `Promise<void>` | Close DB connection |
42
- | `beginTransaction(connId, isolationLevel?)` | `Promise<void>` | Begin transaction |
43
- | `commitTransaction(connId)` | `Promise<void>` | Commit transaction |
44
- | `rollbackTransaction(connId)` | `Promise<void>` | Rollback transaction |
45
- | `executeParametrized(connId, query, params?)` | `Promise<unknown[][]>` | Execute parameterized query |
46
- | `executeDefs(connId, defs, options?)` | `Promise<unknown[][]>` | Execute QueryDef-based queries |
47
- | `bulkInsert(connId, tableName, columnDefs, records)` | `Promise<void>` | Bulk INSERT |
48
-
49
- When a WebSocket connection is closed, all DB connections opened from that socket are automatically cleaned up.
50
-
51
- Use `OrmServiceType` to share method signatures with the client:
52
-
53
- ```typescript
54
- import type { OrmServiceType } from "@simplysm/service-server";
55
- // OrmServiceType = ServiceMethods<typeof OrmService>
56
- ```
57
-
58
- ## AutoUpdateService
59
-
60
- Supports auto-update for client apps. Searches for the latest version files by platform in the client directory. No auth is required.
61
-
62
- ```typescript
63
- import { createServiceServer, AutoUpdateService } from "@simplysm/service-server";
64
-
65
- const server = createServiceServer({
66
- port: 8080,
67
- rootPath: "/app/data",
68
- services: [AutoUpdateService],
69
- });
70
- ```
71
-
72
- Update file structure:
73
-
74
- ```
75
- rootPath/www/{clientName}/{platform}/updates/
76
- 1.0.0.exe (Windows)
77
- 1.0.1.exe
78
- 1.0.0.apk (Android)
79
- 1.0.1.apk
80
- ```
81
-
82
- | Method | Returns | Description |
83
- |--------|---------|------|
84
- | `getLastVersion(platform)` | `Promise<{ version: string; downloadPath: string } \| undefined>` | Returns the latest version and download path for the platform. Returns `undefined` if no update files exist |
85
-
86
- Return value example:
87
-
88
- ```typescript
89
- {
90
- version: "1.0.1",
91
- downloadPath: "/my-app/android/updates/1.0.1.apk",
92
- }
93
- ```
94
-
95
- Use `AutoUpdateServiceType` to share method signatures with the client:
96
-
97
- ```typescript
98
- import type { AutoUpdateServiceType } from "@simplysm/service-server";
99
- // AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>
100
- ```