@schorts/shared-kernel 1.0.0

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 (88) hide show
  1. package/.idx/airules.md +186 -0
  2. package/.idx/dev.nix +54 -0
  3. package/.vscode/settings.json +7 -0
  4. package/CHANGELOG +0 -0
  5. package/README.md +98 -0
  6. package/__tests__/auth/auth-provider.test.ts +45 -0
  7. package/__tests__/auth/require-auth.decorator.test.ts +88 -0
  8. package/__tests__/criteria/criteria.test.ts +159 -0
  9. package/__tests__/criteria/direction.test.ts +11 -0
  10. package/__tests__/criteria/filter-criterion.test.ts +15 -0
  11. package/__tests__/criteria/operator.test.ts +22 -0
  12. package/__tests__/criteria/order.test.ts +14 -0
  13. package/__tests__/domain-events/domain-event-primitives.test.ts +34 -0
  14. package/__tests__/domain-events/domain-event.test.ts +50 -0
  15. package/__tests__/entities/entity.test.ts +114 -0
  16. package/__tests__/formatters/pascal-camel-to-snake.test.ts +19 -0
  17. package/__tests__/http/fetch-http-provider.test.ts +155 -0
  18. package/__tests__/http/http-provider.test.ts +55 -0
  19. package/__tests__/json-api/json-api-connector.test.ts +78 -0
  20. package/__tests__/json-api/json-api-list.test.ts +24 -0
  21. package/__tests__/json-api/json-api-single.test.ts +24 -0
  22. package/__tests__/json-api/url-criteria-builder.test.ts +74 -0
  23. package/__tests__/messages/message.test.ts +16 -0
  24. package/__tests__/models/base-model.test.ts +10 -0
  25. package/__tests__/state-manager/state-manager.test.ts +101 -0
  26. package/__tests__/utils/url/url-with-params-builder.test.ts +39 -0
  27. package/__tests__/value-objects/coordinates-value.test.ts +68 -0
  28. package/__tests__/value-objects/email-value.test.ts +43 -0
  29. package/__tests__/value-objects/enum-value.test.ts +51 -0
  30. package/__tests__/value-objects/integer-value.test.ts +115 -0
  31. package/__tests__/value-objects/phone-value.test.ts +82 -0
  32. package/__tests__/value-objects/slug-value.test.ts +43 -0
  33. package/__tests__/value-objects/string-value.test.ts +121 -0
  34. package/__tests__/value-objects/uuid-value.test.ts +67 -0
  35. package/__tests__/value-objects/value-object.test.ts +25 -0
  36. package/jest.config.js +25 -0
  37. package/package.json +289 -0
  38. package/src/auth/auth-provider.ts +10 -0
  39. package/src/auth/exceptions/index.ts +1 -0
  40. package/src/auth/exceptions/not-authenticated.ts +1 -0
  41. package/src/auth/index.ts +3 -0
  42. package/src/auth/require-auth.decorator.ts +41 -0
  43. package/src/criteria/criteria.ts +51 -0
  44. package/src/criteria/direction.ts +1 -0
  45. package/src/criteria/exceptions/index.ts +2 -0
  46. package/src/criteria/exceptions/limit-not-valid.ts +1 -0
  47. package/src/criteria/exceptions/offset-not-valid.ts +1 -0
  48. package/src/criteria/filter-criterion.ts +6 -0
  49. package/src/criteria/index.ts +7 -0
  50. package/src/criteria/operator.ts +12 -0
  51. package/src/criteria/order.ts +4 -0
  52. package/src/domain-events/domain-event-primitives.ts +7 -0
  53. package/src/domain-events/domain-event.ts +15 -0
  54. package/src/domain-events/index.ts +2 -0
  55. package/src/entities/entity.ts +22 -0
  56. package/src/entities/index.ts +1 -0
  57. package/src/formatters/index.ts +1 -0
  58. package/src/formatters/pascal-camel-to-snake.ts +8 -0
  59. package/src/http/exceptions/http-exception.ts +8 -0
  60. package/src/http/exceptions/index.ts +1 -0
  61. package/src/http/fetch-http-provider.ts +120 -0
  62. package/src/http/http-provider.ts +7 -0
  63. package/src/http/index.ts +4 -0
  64. package/src/json-api/index.ts +4 -0
  65. package/src/json-api/json-api-connector.ts +56 -0
  66. package/src/json-api/json-api-list.ts +13 -0
  67. package/src/json-api/json-api-single.ts +13 -0
  68. package/src/json-api/url-criteria-builder.ts +49 -0
  69. package/src/messages/index.ts +1 -0
  70. package/src/messages/message.ts +3 -0
  71. package/src/models/base-model.ts +3 -0
  72. package/src/models/index.ts +1 -0
  73. package/src/state-manager/index.ts +1 -0
  74. package/src/state-manager/state-manager.ts +28 -0
  75. package/src/utils/index.ts +1 -0
  76. package/src/utils/url/index.ts +1 -0
  77. package/src/utils/url/url-with-params-builder.ts +19 -0
  78. package/src/value-objects/coordinates-value.ts +50 -0
  79. package/src/value-objects/email-value.ts +25 -0
  80. package/src/value-objects/enum-value.ts +25 -0
  81. package/src/value-objects/index.ts +10 -0
  82. package/src/value-objects/integer-value.ts +29 -0
  83. package/src/value-objects/phone-value.ts +53 -0
  84. package/src/value-objects/slug-value.ts +25 -0
  85. package/src/value-objects/string-value.ts +27 -0
  86. package/src/value-objects/uuid-value.ts +34 -0
  87. package/src/value-objects/value-object.ts +7 -0
  88. package/tsconfig.json +46 -0
@@ -0,0 +1,14 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { Order } from "../../src/criteria";
4
+
5
+ type ExpectedOrder = {
6
+ field: string;
7
+ direction: "ASC" | "DESC" | "NONE";
8
+ }
9
+
10
+ describe('Order', () => {
11
+ it('should match the expected type', () => {
12
+ expectTypeOf<Order>().toEqualTypeOf<ExpectedOrder>();
13
+ });
14
+ });
@@ -0,0 +1,34 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { DomainEventPrimitives } from "../../src/domain-events";
4
+
5
+ describe("DomainEventPrimitives", () => {
6
+ it('should have a "id" property of type string', () => {
7
+ expectTypeOf<DomainEventPrimitives>().toHaveProperty('id');
8
+ expectTypeOf<DomainEventPrimitives['id']>().toBeString();
9
+ });
10
+
11
+ it('should have a "occurred_at" property of type string', () => {
12
+ expectTypeOf<DomainEventPrimitives>().toHaveProperty('occurred_at');
13
+ expectTypeOf<DomainEventPrimitives['occurred_at']>().toBeString();
14
+ });
15
+
16
+ it('should have a "type" property of type string', () => {
17
+ expectTypeOf<DomainEventPrimitives>().toHaveProperty('type');
18
+ expectTypeOf<DomainEventPrimitives['type']>().toBeString();
19
+ });
20
+
21
+ it('should have a "version" property of type number', () => {
22
+ expectTypeOf<DomainEventPrimitives>().toHaveProperty('version');
23
+ expectTypeOf<DomainEventPrimitives['version']>().toBeNumber();
24
+ });
25
+
26
+ it('should have a "payload" property of type number', () => {
27
+ type PayloadSchema = {
28
+ name: string;
29
+ };
30
+
31
+ expectTypeOf<DomainEventPrimitives<PayloadSchema>>().toHaveProperty('payload');
32
+ expectTypeOf<DomainEventPrimitives<PayloadSchema>['payload']>().toEqualTypeOf<PayloadSchema>();
33
+ });
34
+ });
@@ -0,0 +1,50 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { DomainEvent, DomainEventPrimitives } from "../../src/domain-events";
4
+
5
+ describe('DomainEvent', () => {
6
+ it('should have a "id" property of type string', () => {
7
+ expectTypeOf<DomainEvent>().toHaveProperty("id");
8
+ expectTypeOf<DomainEvent['id']>().toBeString();
9
+ });
10
+
11
+ it('should have a "occurredAt" property of type Date', () => {
12
+ expectTypeOf<DomainEvent>().toHaveProperty("occurredAt");
13
+ expectTypeOf<DomainEvent['occurredAt']>().toEqualTypeOf<Date>();
14
+ });
15
+
16
+ it('should have a "type" property of type string', () => {
17
+ expectTypeOf<DomainEvent>().toHaveProperty("type");
18
+ expectTypeOf<DomainEvent['type']>().toBeString();
19
+ });
20
+
21
+ it('should have a "version" property of type number', () => {
22
+ expectTypeOf<DomainEvent>().toHaveProperty("version");
23
+ expectTypeOf<DomainEvent['version']>().toBeNumber();
24
+ });
25
+
26
+ it('should have a "payload" property of customt type', () => {
27
+ type PayloadSchema = {
28
+ name: string;
29
+ };
30
+
31
+ expectTypeOf<DomainEvent<PayloadSchema>>().toHaveProperty("payload");
32
+ expectTypeOf<DomainEvent<PayloadSchema>['payload']>().toEqualTypeOf<PayloadSchema>();
33
+ });
34
+
35
+ it('should have a "getEventName" method that returns an string', () => {
36
+ expectTypeOf<DomainEvent>().toHaveProperty('getEventName');
37
+ expectTypeOf<DomainEvent['getEventName']>().toBeFunction();
38
+ expectTypeOf<DomainEvent['getEventName']>().returns.toBeString();
39
+ });
40
+
41
+ it('should declare a "toPrimitives" method', () => {
42
+ type PayloadSchema = {
43
+ name: string;
44
+ };
45
+
46
+ expectTypeOf<DomainEvent>().toHaveProperty('toPrimitives');
47
+ expectTypeOf<DomainEvent['toPrimitives']>().toBeFunction();
48
+ expectTypeOf<DomainEvent<PayloadSchema>['toPrimitives']>().returns.toEqualTypeOf<DomainEventPrimitives<PayloadSchema>>();
49
+ });
50
+ });
@@ -0,0 +1,114 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { Entity } from "../../src/entities";
4
+ import { ValueObject, UUIDValue } from "../../src/value-objects";
5
+ import { DomainEvent, DomainEventPrimitives } from "../../src/domain-events";
6
+
7
+ type Model = {
8
+ id: string;
9
+ name: string;
10
+ };
11
+
12
+ class TestDomainEvent extends DomainEvent {
13
+ static eventName = "shared.v1.tests.test_domain";
14
+
15
+ toPrimitives(): DomainEventPrimitives<{}> {
16
+ return {
17
+ id: this.id,
18
+ occurred_at: this.occurredAt.toString(),
19
+ payload: this.payload,
20
+ type: this.type,
21
+ version: this.version,
22
+ };
23
+ }
24
+
25
+ getEventName() {
26
+ return TestDomainEvent.eventName;
27
+ }
28
+ }
29
+
30
+ class IDValue extends UUIDValue {
31
+ readonly attributeName = "ID";
32
+ }
33
+
34
+ class TestEntity extends Entity<Model> {
35
+ toPrimitives(): Model {
36
+ return {
37
+ id: this.id.toString(),
38
+ name: "test",
39
+ };
40
+ }
41
+ }
42
+
43
+ describe('Entity', () => {
44
+ it('should have a "id" property of type string', () => {
45
+ expectTypeOf<Entity<Model>>().toHaveProperty("id");
46
+ expectTypeOf<Entity<Model>["id"]>().toEqualTypeOf<ValueObject>();
47
+ });
48
+
49
+ it('should have a private "domainEvents" property of type Array<DomainEvent>', () => {
50
+ expectTypeOf<Entity<Model>["domainEvents"]>().toEqualTypeOf<DomainEvent[]>();
51
+ });
52
+
53
+ describe('#pullDomainEvents', () => {
54
+ let testEntity: TestEntity;
55
+
56
+ beforeEach(() => {
57
+ testEntity = new TestEntity(new IDValue(""))
58
+ });
59
+
60
+ it('should return an array of DomainEvent', () => {
61
+ const domainEvents = testEntity.pullDomainEvents();
62
+
63
+ expectTypeOf<typeof domainEvents>().toEqualTypeOf<DomainEvent[]>();
64
+ });
65
+
66
+ it('should return the record domain events', () => {
67
+ const testDomainEvent = new TestDomainEvent(
68
+ "",
69
+ new Date(),
70
+ "Test",
71
+ 1,
72
+ {},
73
+ );
74
+
75
+ testEntity.recordDomainEvent(testDomainEvent);
76
+
77
+ const domainEvents = testEntity.pullDomainEvents();
78
+ const expectedDomainEvents = [testDomainEvent];
79
+
80
+ expect(domainEvents).toEqual(expectedDomainEvents);
81
+ });
82
+ });
83
+
84
+ describe('#recordDomainEvent', () => {
85
+ let testEntity: TestEntity;
86
+
87
+ beforeEach(() => {
88
+ testEntity = new TestEntity(new IDValue(""))
89
+ });
90
+
91
+ it('should record a domain events', () => {
92
+ const testDomainEvent = new TestDomainEvent(
93
+ "",
94
+ new Date(),
95
+ "Test",
96
+ 1,
97
+ {},
98
+ );
99
+
100
+ testEntity.recordDomainEvent(testDomainEvent);
101
+
102
+ const domainEvents = testEntity.pullDomainEvents();
103
+ const expectedDomainEvents = [testDomainEvent];
104
+
105
+ expect(domainEvents).toEqual(expectedDomainEvents);
106
+ });
107
+ });
108
+
109
+ it('should declare a "toPrimitives" method', () => {
110
+ expectTypeOf<Entity<Model>>().toHaveProperty('toPrimitives');
111
+ expectTypeOf<Entity<Model>['toPrimitives']>().toBeFunction();
112
+ expectTypeOf<Entity<Model>['toPrimitives']>().returns.toEqualTypeOf<Model>();
113
+ });
114
+ });
@@ -0,0 +1,19 @@
1
+ import { PascalCamelToSnake } from "../../src/formatters";
2
+
3
+ describe('PascalCamelToSnake', () => {
4
+ describe('.format', () => {
5
+ it('should return a pascal case to snake case', () => {
6
+ const resultID = PascalCamelToSnake.format("ID");
7
+ const resultExampleDAO = PascalCamelToSnake.format("ExampleDAO");
8
+
9
+ expect(resultID).toEqual("id");
10
+ expect(resultExampleDAO).toEqual("example_dao");
11
+ });
12
+
13
+ it('should return a camel case to snake case', () => {
14
+ const resultAlgoClass = PascalCamelToSnake.format("algoClass");
15
+
16
+ expect(resultAlgoClass).toEqual("algo_class");
17
+ });
18
+ });
19
+ });
@@ -0,0 +1,155 @@
1
+ import { FetchHTTPProvider, HTTPException } from "../../src/http";
2
+
3
+ const url = new URL("https://api.example.com/resource");
4
+
5
+ function createMockResponse({
6
+ ok = true,
7
+ status = 200,
8
+ contentType = "application/json",
9
+ body = {},
10
+ }: {
11
+ ok?: boolean;
12
+ status?: number;
13
+ contentType?: string;
14
+ body?: any;
15
+ }): Promise<Response> {
16
+ return Promise.resolve({
17
+ ok,
18
+ status,
19
+ headers: {
20
+ get: (key: string) =>
21
+ key.toLowerCase() === "content-type" ? contentType : null,
22
+ },
23
+ json: async () => body,
24
+ text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
25
+ blob: async () =>
26
+ new Blob([typeof body === "string" ? body : JSON.stringify(body)], {
27
+ type: contentType,
28
+ }),
29
+ } as unknown as Response);
30
+ }
31
+
32
+ describe("FetchHTTPProvider", () => {
33
+ let provider: FetchHTTPProvider;
34
+
35
+ beforeEach(() => {
36
+ provider = new FetchHTTPProvider();
37
+ global.fetch = jest.fn();
38
+ });
39
+
40
+ afterEach(() => {
41
+ jest.resetAllMocks();
42
+ });
43
+
44
+ it("parses JSON response", async () => {
45
+ const mockJson = { message: "success" };
46
+
47
+ (global.fetch as jest.Mock).mockImplementation(() =>
48
+ createMockResponse({ body: mockJson })
49
+ );
50
+
51
+ const result = await provider.get<typeof mockJson>(url);
52
+
53
+ expect(result).toEqual(mockJson);
54
+ });
55
+
56
+ it("parses text response", async () => {
57
+ const mockText = "plain text";
58
+
59
+ (global.fetch as jest.Mock).mockImplementation(() =>
60
+ createMockResponse({ contentType: "text/plain", body: mockText })
61
+ );
62
+
63
+ const result = await provider.get<string>(url);
64
+
65
+ expect(result).toEqual(mockText);
66
+ });
67
+
68
+ it("parses blob response", async () => {
69
+ const mockBinary = "image data";
70
+
71
+ (global.fetch as jest.Mock).mockImplementation(() =>
72
+ createMockResponse({ contentType: "image/png", body: mockBinary })
73
+ );
74
+
75
+ const result = await provider.get<Blob>(url);
76
+
77
+ expect(result).toBeInstanceOf(Blob);
78
+ });
79
+
80
+ it("returns undefined for 204 No Content", async () => {
81
+ (global.fetch as jest.Mock).mockImplementation(() =>
82
+ createMockResponse({ status: 204, body: undefined })
83
+ );
84
+
85
+ const result = await provider.get<undefined>(url);
86
+
87
+ expect(result).toBeUndefined();
88
+ });
89
+
90
+ it("throws HTTPException on failed JSON response", async () => {
91
+ const errorJson = { title: "Unauthorized", code: 401 };
92
+
93
+ (global.fetch as jest.Mock).mockImplementation(() =>
94
+ createMockResponse({ ok: false, status: 401, body: errorJson })
95
+ );
96
+
97
+ await expect(provider.get(url)).rejects.toThrow(HTTPException);
98
+ });
99
+
100
+ it("throws HTTPException on failed text response", async () => {
101
+ const errorText = "Forbidden";
102
+
103
+ (global.fetch as jest.Mock).mockImplementation(() =>
104
+ createMockResponse({
105
+ ok: false,
106
+ status: 403,
107
+ contentType: "text/plain",
108
+ body: errorText,
109
+ })
110
+ );
111
+
112
+ await expect(provider.get(url)).rejects.toThrow(HTTPException);
113
+ });
114
+
115
+ it("deduplicates ongoing requests", async () => {
116
+ const mockJson = { message: "deduplicated" };
117
+
118
+ (global.fetch as jest.Mock).mockImplementation(() =>
119
+ createMockResponse({ body: mockJson })
120
+ );
121
+
122
+ const promise1 = provider.get<typeof mockJson>(url);
123
+ const promise2 = provider.get<typeof mockJson>(url);
124
+
125
+ expect(promise1).toStrictEqual(promise2);
126
+
127
+ const result = await promise1;
128
+
129
+ expect(result).toEqual(mockJson);
130
+ expect(global.fetch).toHaveBeenCalledTimes(1);
131
+ });
132
+
133
+ it("cleans up ongoingRequests after completion", async () => {
134
+ const mockJson = { message: "done" };
135
+
136
+ (global.fetch as jest.Mock).mockImplementation(() =>
137
+ createMockResponse({ body: mockJson })
138
+ );
139
+
140
+ await provider.get<typeof mockJson>(url);
141
+
142
+ const key = `GET:${url.href}:`;
143
+ const internalMap = (provider as any).ongoingRequests as Map<string, Promise<any>>;
144
+
145
+ expect(internalMap.has(key)).toBe(false);
146
+ });
147
+
148
+ it("throws if fetch returns undefined", async () => {
149
+ (global.fetch as jest.Mock).mockImplementation(() =>
150
+ Promise.resolve(undefined as any)
151
+ );
152
+
153
+ await expect(provider.get(url)).rejects.toThrow(HTTPException);
154
+ });
155
+ });
@@ -0,0 +1,55 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { HTTPProvider } from "../../src/http";
4
+
5
+ describe('HTTPProvider', () => {
6
+ describe('#get', () => {
7
+ it('should receive an URL', () => {
8
+ expectTypeOf<HTTPProvider["get"]>().parameters.toEqualTypeOf<[URL]>();
9
+ });
10
+
11
+ it('should return a Promise<ResponseType>', () => {
12
+ expectTypeOf<HTTPProvider["get"]>().returns.toEqualTypeOf<Promise<unknown>>();
13
+ });
14
+ });
15
+
16
+ describe('#post', () => {
17
+ it('should receive an URL and a body', () => {
18
+ expectTypeOf<HTTPProvider["post"]>().parameters.toEqualTypeOf<[URL, unknown]>();
19
+ });
20
+
21
+ it('should return a Promise<ResponseType>', () => {
22
+ expectTypeOf<HTTPProvider["post"]>().returns.toEqualTypeOf<Promise<unknown>>();
23
+ });
24
+ });
25
+
26
+ describe('#put', () => {
27
+ it('should receive an URL and a body', () => {
28
+ expectTypeOf<HTTPProvider["put"]>().parameters.toEqualTypeOf<[URL, unknown]>();
29
+ });
30
+
31
+ it('should return a Promise<ResponseType>', () => {
32
+ expectTypeOf<HTTPProvider["put"]>().returns.toEqualTypeOf<Promise<unknown>>();
33
+ });
34
+ });
35
+
36
+ describe('#patch', () => {
37
+ it('should receive an URL and a body', () => {
38
+ expectTypeOf<HTTPProvider["patch"]>().parameters.toEqualTypeOf<[URL, unknown]>();
39
+ });
40
+
41
+ it('should return a Promise<ResponseType>', () => {
42
+ expectTypeOf<HTTPProvider["patch"]>().returns.toEqualTypeOf<Promise<unknown>>();
43
+ });
44
+ });
45
+
46
+ describe('#delete', () => {
47
+ it('should receive an URL', () => {
48
+ expectTypeOf<HTTPProvider["delete"]>().parameters.toEqualTypeOf<[URL]>();
49
+ });
50
+
51
+ it('should return a Promise<ResponseType>', () => {
52
+ expectTypeOf<HTTPProvider["delete"]>().returns.toEqualTypeOf<Promise<unknown>>();
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,78 @@
1
+ import { JSONAPIConnector } from "../../src/json-api";
2
+ import { Criteria } from "../../src/criteria";
3
+ import type { HTTPProvider } from "../../src/http";
4
+
5
+ describe("JSONAPIConnector", () => {
6
+ const base = new URL("https://api.example.com/users");
7
+ let http: jest.Mocked<HTTPProvider>;
8
+ let connector: JSONAPIConnector;
9
+
10
+ beforeEach(() => {
11
+ http = {
12
+ get: jest.fn(),
13
+ post: jest.fn(),
14
+ patch: jest.fn(),
15
+ put: jest.fn(),
16
+ delete: jest.fn(),
17
+ };
18
+ connector = new JSONAPIConnector(http);
19
+ });
20
+
21
+ it("calls findOne with correct URL and criteria", async () => {
22
+ const criteria = new Criteria().where("status", "EQUAL", "active");
23
+ const include = ["roles"];
24
+
25
+ await connector.findOne(base, criteria, include);
26
+
27
+ expect(http.get).toHaveBeenCalledWith(expect.any(URL));
28
+
29
+ const calledUrl = http.get.mock.calls[0][0];
30
+
31
+ expect(calledUrl.searchParams.get("filter[status]")).toBe("active");
32
+ expect(calledUrl.href).toContain("include=roles");
33
+ });
34
+
35
+ it("calls findMany with sorting and pagination", async () => {
36
+ const criteria = new Criteria()
37
+ .orderBy("createdAt", "DESC")
38
+ .limitResults(10)
39
+ .offsetResults(20);
40
+
41
+ await connector.findMany(base, criteria);
42
+
43
+ const calledUrl = http.get.mock.calls[0][0];
44
+
45
+ expect(calledUrl.href).toContain("sort=-createdAt");
46
+ expect(calledUrl.searchParams.get("page[limit]")).toBe("10");
47
+ expect(calledUrl.searchParams.get("page[offset]")).toBe("20");
48
+ });
49
+
50
+ it("calls create with correct payload", async () => {
51
+ const payload = {
52
+ type: "users",
53
+ attributes: { name: "Jorge", email: "jorge@example.com" },
54
+ };
55
+
56
+ await connector.create(base, payload);
57
+
58
+ expect(http.post).toHaveBeenCalledWith(base, { data: payload });
59
+ });
60
+
61
+ it("calls update with correct payload", async () => {
62
+ const payload = {
63
+ id: "123",
64
+ type: "users",
65
+ attributes: { name: "Jorge Updated" },
66
+ };
67
+
68
+ await connector.update(base, payload);
69
+
70
+ expect(http.patch).toHaveBeenCalledWith(base, { data: payload });
71
+ });
72
+
73
+ it("calls delete with correct URL", async () => {
74
+ await connector.delete(base);
75
+
76
+ expect(http.delete).toHaveBeenCalledWith(base);
77
+ });
78
+ });
@@ -0,0 +1,24 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { JSONAPIList } from "../../src/json-api";
4
+ import { BaseModel } from "../../src/models";
5
+
6
+ type ExpectedJSONAPIList<EntityAttributes> = {
7
+ data: Array<{
8
+ id: string;
9
+ type: string;
10
+ attributes: Omit<EntityAttributes, "id">;
11
+ }>;
12
+ included?: Array<{
13
+ id: string;
14
+ type: string;
15
+ attributes: Record<string, any>;
16
+ }>;
17
+ meta?: Record<string, any>;
18
+ };
19
+
20
+ describe('JSONAPIList', () => {
21
+ it('should match the expected schema', () => {
22
+ expectTypeOf<JSONAPIList<BaseModel>>().toEqualTypeOf<ExpectedJSONAPIList<BaseModel>>()
23
+ });
24
+ });
@@ -0,0 +1,24 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { JSONAPISingle } from "../../src/json-api";
4
+ import { BaseModel } from "../../src/models";
5
+
6
+ type ExpectedJSONAPISingle<EntityAttributes> = {
7
+ data: {
8
+ id: string;
9
+ type: string;
10
+ attributes: Omit<EntityAttributes, "id">;
11
+ };
12
+ included?: Array<{
13
+ id: string;
14
+ type: string;
15
+ attributes: Omit<Record<string, any>, "id">;
16
+ }>;
17
+ meta?: Record<string, any>;
18
+ };
19
+
20
+ describe('JSONAPISingle', () => {
21
+ it('should match the expected schema', () => {
22
+ expectTypeOf<JSONAPISingle<BaseModel>>().toEqualTypeOf<ExpectedJSONAPISingle<BaseModel>>()
23
+ });
24
+ });
@@ -0,0 +1,74 @@
1
+ import { URLCriteriaBuilder } from "../../src/json-api";
2
+ import { Criteria } from "../../src/criteria";
3
+
4
+ const base = new URL("https://api.example.com/resources");
5
+
6
+ describe("URLCriteriaBuilder", () => {
7
+ it("encodes EQUAL filters as filter[field]=value", () => {
8
+ const criteria = new Criteria().where("status", "EQUAL", "active");
9
+ const url = new URLCriteriaBuilder(base, criteria).build();
10
+
11
+ expect(url.searchParams.get("filter[status]")).toBe("active");
12
+ });
13
+
14
+ it("encodes IN filters as comma-separated values", () => {
15
+ const criteria = new Criteria().where("role", "IN", ["admin", "editor"]);
16
+ const url = new URLCriteriaBuilder(base, criteria).build();
17
+
18
+ expect(url.searchParams.get("filter[role]")).toBe("admin,editor");
19
+ });
20
+
21
+ it("encodes other operators as filter[field][operator]=value", () => {
22
+ const criteria = new Criteria().where("createdAt", "GREATER_THAN", "2023-01-01");
23
+ const url = new URLCriteriaBuilder(base, criteria).build();
24
+
25
+ expect(url.searchParams.get("filter[createdAt][GREATER_THAN]")).toBe("2023-01-01");
26
+ });
27
+
28
+ it("supports nested filters using dot notation", () => {
29
+ const criteria = new Criteria().where("roles.name", "EQUAL", "admin");
30
+ const url = new URLCriteriaBuilder(base, criteria).build();
31
+
32
+ expect(url.searchParams.get("filter[roles.name]")).toBe("admin");
33
+ });
34
+
35
+ it("encodes sorting correctly", () => {
36
+ const criteria = new Criteria()
37
+ .orderBy("createdAt", "DESC")
38
+ .orderBy("name", "ASC");
39
+ const url = new URLCriteriaBuilder(base, criteria).build();
40
+
41
+ expect(url.searchParams.get("sort")).toBe("-createdAt,name");
42
+ });
43
+
44
+ it("encodes pagination correctly", () => {
45
+ const criteria = new Criteria().limitResults(20).offsetResults(40);
46
+ const url = new URLCriteriaBuilder(base, criteria).build();
47
+
48
+ expect(url.searchParams.get("page[limit]")).toBe("20");
49
+ expect(url.searchParams.get("page[offset]")).toBe("40");
50
+ });
51
+
52
+ it("adds include parameters", () => {
53
+ const url = new URLCriteriaBuilder(base, undefined, ["roles", "permissions"]).build();
54
+
55
+ expect(url.searchParams.getAll("include")).toEqual(["roles", "permissions"]);
56
+ });
57
+
58
+ it("builds full query with filters, sort, pagination, and includes", () => {
59
+ const criteria = new Criteria()
60
+ .where("status", "EQUAL", "active")
61
+ .where("roles.name", "EQUAL", "admin")
62
+ .orderBy("createdAt", "DESC")
63
+ .limitResults(10)
64
+ .offsetResults(30);
65
+ const url = new URLCriteriaBuilder(base, criteria, ["roles"]).build();
66
+
67
+ expect(url.searchParams.get("filter[status]")).toBe("active");
68
+ expect(url.searchParams.get("filter[roles.name]")).toBe("admin");
69
+ expect(url.searchParams.get("sort")).toBe("-createdAt");
70
+ expect(url.searchParams.get("page[limit]")).toBe("10");
71
+ expect(url.searchParams.get("page[offset]")).toBe("30");
72
+ expect(url.searchParams.getAll("include")).toEqual(["roles"]);
73
+ });
74
+ });
@@ -0,0 +1,16 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { Message } from "../../src/messages";
4
+
5
+ type MessagePrimitives = {
6
+ id: string;
7
+ };
8
+
9
+ describe("Message", () => {
10
+ it('should declare a "toPrimitives" method', () => {
11
+ expectTypeOf<Message>().toHaveProperty('toPrimitives');
12
+ expectTypeOf<Message['toPrimitives']>().toBeFunction();
13
+ expectTypeOf<Message<MessagePrimitives>['toPrimitives']>().returns.toEqualTypeOf<MessagePrimitives>();
14
+ });
15
+ });
16
+
@@ -0,0 +1,10 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ import { BaseModel } from "../../src/models";
4
+
5
+ describe("BaseModel", () => {
6
+ it('should have a "id" property of type string or number', () => {
7
+ expectTypeOf<BaseModel>().toHaveProperty("id");
8
+ expectTypeOf<BaseModel['id']>().toEqualTypeOf<string | number>();
9
+ });
10
+ });