sdc_client 0.58.6 → 0.158.1

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 (39) hide show
  1. package/.github/workflows/node.js.yml +2 -2
  2. package/.idea/inspectionProfiles/Project_Default.xml +17 -0
  3. package/.idea/workspace.xml +122 -23
  4. package/.readthedocs.yaml +13 -0
  5. package/dist/index.js +70 -51
  6. package/dist/ugly.index.js +1 -1
  7. package/docs/api-reference.rst +225 -0
  8. package/docs/conf.py +11 -0
  9. package/docs/controllers.rst +228 -0
  10. package/docs/events-and-dom.rst +120 -0
  11. package/docs/getting-started.rst +143 -0
  12. package/docs/index.rst +22 -0
  13. package/docs/models.rst +351 -0
  14. package/docs/overview.rst +66 -0
  15. package/docs/requirements.txt +1 -0
  16. package/eslint.config.js +60 -0
  17. package/gulp/gulp.jsx +15 -13
  18. package/package.json +13 -3
  19. package/src/index.js +44 -11
  20. package/src/simpleDomControl/AbstractSDC.js +287 -286
  21. package/src/simpleDomControl/sdc_controller.js +157 -132
  22. package/src/simpleDomControl/sdc_dom_events.js +99 -88
  23. package/src/simpleDomControl/sdc_events.js +39 -40
  24. package/src/simpleDomControl/sdc_main.js +88 -67
  25. package/src/simpleDomControl/sdc_model.js +1513 -0
  26. package/src/simpleDomControl/sdc_params.js +44 -29
  27. package/src/simpleDomControl/sdc_server_call.js +153 -154
  28. package/src/simpleDomControl/sdc_socket.js +8 -829
  29. package/src/simpleDomControl/sdc_test_utils.js +77 -80
  30. package/src/simpleDomControl/sdc_utils.js +295 -176
  31. package/src/simpleDomControl/sdc_view.js +245 -177
  32. package/test/model.test.js +332 -0
  33. package/test/models/Author.js +145 -0
  34. package/test/models/Book.js +112 -0
  35. package/test/models/BookContent.js +114 -0
  36. package/test/models/SdcUser.js +75 -0
  37. package/test/models/src.js +8 -0
  38. package/test/sdc_model_dates.ai.test.js +57 -0
  39. package/test/sdc_server_call.ai.test.js +267 -0
@@ -0,0 +1,75 @@
1
+ import {SdcModel} from '../../src/index.js';
2
+
3
+ export default class SdcUser extends SdcModel {
4
+
5
+ static fields = {
6
+ "id": {
7
+ "type": "BigAutoField",
8
+ "required": false,
9
+ "max_length": null,
10
+ "is_relation": false,
11
+ "many_to_many": null,
12
+ "one_to_many": null,
13
+ "many_to_one": null,
14
+ "one_to_one": null,
15
+ "related_model": null,
16
+ "remote_field": null
17
+ },
18
+ "username": {
19
+ "type": "CharField",
20
+ "required": false,
21
+ "max_length": 255,
22
+ "is_relation": false,
23
+ "many_to_many": null,
24
+ "one_to_many": null,
25
+ "many_to_one": null,
26
+ "one_to_one": null,
27
+ "related_model": null,
28
+ "remote_field": null
29
+ }
30
+ }
31
+
32
+ constructor(data = {}) {
33
+ super("SdcUser");
34
+ this._id = null;
35
+ this._username = null;
36
+ this.setValues(data);
37
+ }
38
+
39
+ setValues(data = {}) {
40
+ try {
41
+ this.id = data.id ?? data.pk ?? null;
42
+ } catch {}
43
+ try {
44
+ this.username = data.username ?? null;
45
+ } catch {}
46
+ }
47
+
48
+ set id(value){
49
+ this.setid(value);
50
+ this._updateForm('id');
51
+ }
52
+
53
+ set username(value){
54
+ this.setusername(value);
55
+ this._updateForm('username');
56
+ }
57
+
58
+ setid(value){
59
+ this.validate(value, SdcUser.fields.id);
60
+ this._id = this.parseValue(value, SdcUser.fields.id);
61
+ }
62
+
63
+ setusername(value){
64
+ this.validate(value, SdcUser.fields.username);
65
+ this._username = this.parseValue(value, SdcUser.fields.username);
66
+ }
67
+
68
+ get id(){
69
+ return this._id;
70
+ }
71
+
72
+ get username(){
73
+ return this._username;
74
+ }
75
+ }
@@ -0,0 +1,8 @@
1
+ import BookContent from './BookContent.js';
2
+ import Book from './Book.js';
3
+ import Author from './Author.js';
4
+ import { registerModel } from '../../src/index.js';
5
+
6
+ registerModel("Author", Author);
7
+ registerModel("Book", Book);
8
+ registerModel("BookContent", BookContent);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { jest } from "@jest/globals";
5
+ import SdcModel from "../src/simpleDomControl/sdc_model.js";
6
+
7
+ const FROZEN_NOW = new Date("2026-04-03T10:15:30.000Z");
8
+
9
+ describe("SdcModel date parsing and validation", () => {
10
+ beforeEach(() => {
11
+ jest.useFakeTimers();
12
+ jest.setSystemTime(FROZEN_NOW);
13
+ });
14
+
15
+ afterEach(() => {
16
+ jest.useRealTimers();
17
+ });
18
+
19
+ test("parseValue converts ISO datetime strings to timestamps deterministically", () => {
20
+ const model = new SdcModel("AuditLog");
21
+
22
+ const parsed = model.parseValue("2026-04-03T10:15:30.000Z", {
23
+ type: "DateTimeField",
24
+ });
25
+
26
+ expect(parsed).toBe(Date.parse("2026-04-03T10:15:30.000Z"));
27
+ expect(parsed).toBe(FROZEN_NOW.getTime());
28
+ });
29
+
30
+ test("validate accepts valid date and datetime payloads from the server", () => {
31
+ const model = new SdcModel("AuditLog");
32
+
33
+ expect(() =>
34
+ model.validate("2026-04-03", {
35
+ type: "DateField",
36
+ required: true,
37
+ }),
38
+ ).not.toThrow();
39
+ expect(() =>
40
+ model.validate("2026-04-03T10:15:30.000Z", {
41
+ type: "DateTimeField",
42
+ required: true,
43
+ }),
44
+ ).not.toThrow();
45
+ });
46
+
47
+ test("validate rejects invalid date payloads with a stable error", () => {
48
+ const model = new SdcModel("AuditLog");
49
+
50
+ expect(() =>
51
+ model.validate("not-a-date", {
52
+ type: "DateTimeField",
53
+ required: true,
54
+ }),
55
+ ).toThrow("Must be a valid date");
56
+ });
57
+ });
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { jest } from "@jest/globals";
5
+ import $ from "jquery";
6
+
7
+ window.$ = $;
8
+ global.$ = $;
9
+
10
+ const FROZEN_NOW = new Date("2026-04-03T10:15:30.000Z");
11
+
12
+ class EventSink {
13
+ constructor() {
14
+ this.calls = [];
15
+ }
16
+
17
+ pushMsg(...args) {
18
+ this.calls.push(["pushMsg", ...args]);
19
+ }
20
+
21
+ pushErrorMsg(...args) {
22
+ this.calls.push(["pushErrorMsg", ...args]);
23
+ }
24
+
25
+ onNavLink(...args) {
26
+ this.calls.push(["onNavLink", ...args]);
27
+ }
28
+
29
+ serverEvent(...args) {
30
+ this.calls.push(["serverEvent", ...args]);
31
+ }
32
+ }
33
+
34
+ class MockWebSocket {
35
+ static instances = [];
36
+
37
+ constructor(url) {
38
+ this.url = url;
39
+ this.send = jest.fn();
40
+ this.close = jest.fn(() => {
41
+ if (this.onclose) {
42
+ this.onclose({ code: 1000, reason: "client-close" });
43
+ }
44
+ });
45
+ MockWebSocket.instances.push(this);
46
+ }
47
+
48
+ emitOpen() {
49
+ this.onopen && this.onopen();
50
+ }
51
+
52
+ emitMessage(data) {
53
+ this.onmessage && this.onmessage({ data: JSON.stringify(data) });
54
+ }
55
+
56
+ emitClose(event = { code: 1006, reason: "server-close" }) {
57
+ this.onclose && this.onclose(event);
58
+ }
59
+ }
60
+
61
+ async function loadFreshModules() {
62
+ jest.resetModules();
63
+ window.$ = $;
64
+ global.$ = $;
65
+ const events = await import("../src/simpleDomControl/sdc_events.js");
66
+ const serverCall = await import("../src/simpleDomControl/sdc_server_call.js");
67
+ return { events, serverCall };
68
+ }
69
+
70
+ async function createEventSink(events) {
71
+ const sink = new EventSink();
72
+ ["pushMsg", "pushErrorMsg", "onNavLink", "serverEvent"].forEach((eventName) => {
73
+ events.setEvent(eventName);
74
+ events.on(eventName, sink);
75
+ });
76
+ return sink;
77
+ }
78
+
79
+ describe("sdc_server_call", () => {
80
+ beforeEach(() => {
81
+ jest.useFakeTimers();
82
+ jest.setSystemTime(FROZEN_NOW);
83
+ jest.restoreAllMocks();
84
+ window.CSRF_TOKEN = "csrf-token";
85
+ window.SERVER_CALL_VIA_WEB_SOCKET = false;
86
+ MockWebSocket.instances = [];
87
+ global.WebSocket = MockWebSocket;
88
+ });
89
+
90
+ afterEach(() => {
91
+ jest.runOnlyPendingTimers();
92
+ jest.useRealTimers();
93
+ jest.restoreAllMocks();
94
+ delete global.WebSocket;
95
+ });
96
+
97
+ test("posts server calls and forwards success messages", async () => {
98
+ const { events, serverCall } = await loadFreshModules();
99
+ const sink = await createEventSink(events);
100
+ const postSpy = jest.spyOn($, "post").mockImplementation((options) => {
101
+ const xhr = { setRequestHeader: jest.fn() };
102
+ options.beforeSend(xhr, options);
103
+ expect(xhr.setRequestHeader).toHaveBeenCalledWith("X-CSRFToken", "csrf-token");
104
+ return Promise.resolve({
105
+ _return_data: {
106
+ header: "Saved",
107
+ msg: "Server accepted request",
108
+ data: { id: 7 },
109
+ },
110
+ });
111
+ });
112
+
113
+ const result = await serverCall.callServer(
114
+ "demo_app",
115
+ "demo-controller",
116
+ "/sdc_view/demo_app",
117
+ "saveItem",
118
+ { name: "Ada" },
119
+ );
120
+
121
+ expect(postSpy).toHaveBeenCalledWith(
122
+ expect.objectContaining({
123
+ url: "/sdc_view/demo_app",
124
+ data: {
125
+ data: JSON.stringify({ name: "Ada" }),
126
+ _sdc_func_name: "saveItem",
127
+ _method: "sdc_server_call",
128
+ },
129
+ }),
130
+ );
131
+ expect(result).toEqual({
132
+ header: "Saved",
133
+ msg: "Server accepted request",
134
+ data: { id: 7 },
135
+ });
136
+ expect(sink.calls).toEqual([["pushMsg", "Saved", "Server accepted request"]]);
137
+ });
138
+
139
+ test("marks failed post responses as errors and forwards error messages", async () => {
140
+ const { events, serverCall } = await loadFreshModules();
141
+ const sink = await createEventSink(events);
142
+ const responseError = {
143
+ responseJSON: {
144
+ header: "Invalid",
145
+ msg: "Payload was rejected",
146
+ },
147
+ };
148
+
149
+ jest.spyOn($, "post").mockRejectedValue(responseError);
150
+
151
+ await expect(
152
+ serverCall.callServer(
153
+ "demo_app",
154
+ "demo-controller",
155
+ "/sdc_view/demo_app",
156
+ "saveItem",
157
+ { bad: true },
158
+ ),
159
+ ).rejects.toBe(responseError);
160
+
161
+ expect(responseError.responseJSON.is_error).toBe(true);
162
+ expect(sink.calls).toEqual([["pushErrorMsg", "Invalid", "Payload was rejected"]]);
163
+ });
164
+
165
+ test("sends websocket calls, resolves recalls, and forwards server events", async () => {
166
+ window.SERVER_CALL_VIA_WEB_SOCKET = true;
167
+ const { events, serverCall } = await loadFreshModules();
168
+ const sink = await createEventSink(events);
169
+
170
+ const connection = serverCall.isConnected();
171
+ expect(MockWebSocket.instances).toHaveLength(1);
172
+ const socket = MockWebSocket.instances[0];
173
+ expect(socket.url).toBe("ws://localhost/sdc_ws/ws/");
174
+ socket.emitOpen();
175
+ await connection;
176
+
177
+ const responsePromise = serverCall.callServer(
178
+ "demo_app",
179
+ "demo-controller",
180
+ "/ignored",
181
+ "loadDashboard",
182
+ { filter: "recent" },
183
+ );
184
+
185
+ await Promise.resolve();
186
+
187
+ expect(socket.send).toHaveBeenCalledTimes(1);
188
+ const requestPayload = JSON.parse(socket.send.mock.calls[0][0]);
189
+ expect(requestPayload).toMatchObject({
190
+ event: "sdc_call",
191
+ controller: "demo-controller",
192
+ app: "demo_app",
193
+ function: "loadDashboard",
194
+ args: { filter: "recent" },
195
+ });
196
+
197
+ socket.emitMessage({
198
+ header: "Ready",
199
+ msg: "Dashboard loaded",
200
+ type: "sdc_event",
201
+ event: "serverEvent",
202
+ payload: { generatedAt: FROZEN_NOW.toISOString() },
203
+ });
204
+ socket.emitMessage({
205
+ id: requestPayload.id,
206
+ type: "sdc_recall",
207
+ data: {
208
+ rows: 3,
209
+ generatedAt: FROZEN_NOW.toISOString(),
210
+ },
211
+ });
212
+
213
+ await expect(responsePromise).resolves.toEqual({
214
+ rows: 3,
215
+ generatedAt: "2026-04-03T10:15:30.000Z",
216
+ });
217
+ expect(sink.calls).toEqual([
218
+ ["pushMsg", "Ready", "Dashboard loaded"],
219
+ ["serverEvent", { generatedAt: "2026-04-03T10:15:30.000Z" }],
220
+ ]);
221
+ });
222
+
223
+ test("forwards websocket redirects through onNavLink", async () => {
224
+ window.SERVER_CALL_VIA_WEB_SOCKET = true;
225
+ const { events, serverCall } = await loadFreshModules();
226
+ const sink = await createEventSink(events);
227
+
228
+ const connection = serverCall.isConnected();
229
+ const socket = MockWebSocket.instances[0];
230
+ socket.emitOpen();
231
+ await connection;
232
+
233
+ socket.emitMessage({
234
+ type: "sdc_redirect",
235
+ link: "/target/path",
236
+ });
237
+
238
+ expect(sink.calls).toEqual([["onNavLink", "/target/path"]]);
239
+ });
240
+
241
+ test("rejects open websocket requests on close and reconnects after one second", async () => {
242
+ window.SERVER_CALL_VIA_WEB_SOCKET = true;
243
+ const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
244
+ const { serverCall } = await loadFreshModules();
245
+
246
+ const pendingResponse = serverCall.callServer(
247
+ "demo_app",
248
+ "demo-controller",
249
+ "/ignored",
250
+ "loadDashboard",
251
+ { filter: "recent" },
252
+ );
253
+
254
+ const firstSocket = MockWebSocket.instances[0];
255
+ firstSocket.emitOpen();
256
+ await Promise.resolve();
257
+
258
+ firstSocket.emitClose({ code: 1011, reason: "server-failure" });
259
+
260
+ await expect(pendingResponse).rejects.toEqual({});
261
+ expect(consoleErrorSpy).toHaveBeenCalledWith("SDC Socket closed unexpectedly");
262
+
263
+ expect(MockWebSocket.instances).toHaveLength(1);
264
+ jest.advanceTimersByTime(1000);
265
+ expect(MockWebSocket.instances).toHaveLength(2);
266
+ });
267
+ });