sdc_client 0.58.5 → 0.158.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 (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 +1510 -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 -821
  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,332 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { jest } from "@jest/globals";
6
+ import $ from "jquery";
7
+ import { registerModel } from "../src/index.js";
8
+ import Author from "./models/Author.js";
9
+ import Book from "./models/Book.js";
10
+ import BookContent from "./models/BookContent.js";
11
+ import SdcUser from "./models/SdcUser.js";
12
+
13
+ window.$ = $;
14
+ global.$ = $;
15
+
16
+ class MockModelSocket {
17
+ static server = null;
18
+
19
+ constructor(url) {
20
+ this.url = url;
21
+ this.sentMessages = [];
22
+ queueMicrotask(() => {
23
+ this.onopen && this.onopen();
24
+ });
25
+ }
26
+
27
+ send(rawMessage) {
28
+ this.sentMessages.push(JSON.parse(rawMessage));
29
+ MockModelSocket.server.receive(this, JSON.parse(rawMessage));
30
+ }
31
+
32
+ close() {
33
+ this.onclose && this.onclose({ code: 1000, reason: "client-close" });
34
+ }
35
+ }
36
+
37
+ class MockModelSocketServer {
38
+ constructor() {
39
+ this.records = {
40
+ Author: [
41
+ { pk: 1, id: 1, fields: {name: "Ada Lovelace", age: 36, book_set: [11]} },
42
+ { pk: 2, id: 2, fields: {name: "Grace Hopper", age: 85, book_set: [21, 22]} },
43
+ ],
44
+ Book: [
45
+ { pk: 11, id: 11, fields: {title: "Notes", author: 1 }},
46
+ { pk: 21, id: 21, fields: {title: "Compiler Notes", author: 2 }},
47
+ { pk: 22, id: 22, fields: {title: "COBOL", author: 2 }},
48
+ ],
49
+ BookContent: [],
50
+ SdcUser: [{ pk: 7, id: 7, fields: {username: "tester" }}],
51
+ };
52
+ }
53
+
54
+ emit(socket, payload) {
55
+ queueMicrotask(() => {
56
+ socket.onmessage && socket.onmessage({ data: JSON.stringify(payload) });
57
+ });
58
+ }
59
+
60
+ receive(socket, message) {
61
+ const { event_type: eventType, event_id: eventId, args } = message;
62
+
63
+ if (eventType === "connect") {
64
+ this.emit(socket, {
65
+ type: "connect",
66
+ event_id: eventId,
67
+ args: {},
68
+ });
69
+ return;
70
+ }
71
+
72
+ if (eventType === "load") {
73
+ const rows = this.filterRows(args.model_name, args.model_query);
74
+ this.emit(socket, {
75
+ type: "load",
76
+ event_id: eventId,
77
+ args: {
78
+ data: JSON.stringify(rows),
79
+ },
80
+ });
81
+ return;
82
+ }
83
+
84
+ if (eventType === "save") {
85
+ const saved = this.saveRow(args.model_name, args.data);
86
+ this.emit(socket, {
87
+ event_id: eventId,
88
+ data: {
89
+ instance: JSON.stringify([saved]),
90
+ },
91
+ });
92
+ return;
93
+ }
94
+
95
+ if (eventType === "create") {
96
+ const created = this.createRow(args.model_name, args.data);
97
+ this.emit(socket, {
98
+ event_id: eventId,
99
+ data: {
100
+ instance: JSON.stringify([created]),
101
+ },
102
+ });
103
+ }
104
+ }
105
+
106
+ filterRows(modelName, query = {}) {
107
+ return this.records[modelName].filter((row) =>
108
+ Object.entries(query || {}).every(([key, value]) => row[key] === value || row.fields[key] === value),
109
+ );
110
+ }
111
+
112
+ saveRow(modelName, data) {
113
+ const records = this.records[modelName];
114
+ const id = data.pk ?? data.id;
115
+ const idx = records.findIndex((row) => row.id === id);
116
+
117
+ records[idx].fields = {
118
+ ...records[idx].fields,
119
+ ...data
120
+ };
121
+ return records[idx];
122
+ }
123
+
124
+ createRow(modelName, data) {
125
+ const records = this.records[modelName];
126
+ const newId = Math.max(0, ...records.map((row) => row.id ?? 0)) + 1;
127
+ if (data.hasOwnProperty('id')) {
128
+ delete data.id;
129
+ }
130
+ const created = {
131
+ fields: {...data},
132
+ id: newId,
133
+ };
134
+
135
+ records.push(created);
136
+ return created;
137
+ }
138
+ }
139
+
140
+ describe("model fixtures", () => {
141
+ beforeAll(() => {
142
+ registerModel("Author", Author);
143
+ registerModel("Book", Book);
144
+ registerModel("BookContent", BookContent);
145
+ registerModel("SdcUser", SdcUser);
146
+ });
147
+
148
+ beforeEach(() => {
149
+ window.location.host = "localhost";
150
+ window.location.protocol = "http:";
151
+ MockModelSocket.server = new MockModelSocketServer();
152
+ global.WebSocket = MockModelSocket;
153
+ jest.restoreAllMocks();
154
+ });
155
+
156
+ afterEach(() => {
157
+ $("body").empty();
158
+ delete global.WebSocket;
159
+ });
160
+
161
+ test("Author stores scalar fields and related book ids from fixture payloads", () => {
162
+ const author = new Author({
163
+ id: 3,
164
+ name: "Ada Lovelace",
165
+ age: 36,
166
+ book_set: [11, 15],
167
+ });
168
+
169
+ expect(author.id).toBe(3);
170
+ expect(author.name).toBe("Ada Lovelace");
171
+ expect(author.age).toBe(36);
172
+ expect(author.book_set.getIds()).toEqual([11, 15]);
173
+ expect(author.book_set.modelName).toBe("Book");
174
+ });
175
+
176
+ test("Book serializes many-to-one relations as a single related id", () => {
177
+ const book = new Book({
178
+ id: 8,
179
+ title: "Analytical Engine Notes",
180
+ });
181
+
182
+ book.author = 3;
183
+
184
+ expect(book.author.id).toBe(3);
185
+ expect(book.serialize()).toEqual({
186
+ id: 8,
187
+ title: "Analytical Engine Notes",
188
+ author: 3,
189
+ });
190
+ });
191
+
192
+ test("Author serializes one-to-many relations as an id list", () => {
193
+ const author = new Author({
194
+ id: 5,
195
+ name: "Grace Hopper",
196
+ age: 85,
197
+ });
198
+ const firstBook = new Book({ id: 21, title: "Compiler Notes" });
199
+ const secondBook = new Book({ id: 22, title: "COBOL" });
200
+
201
+ firstBook.author = 5;
202
+ secondBook.author = 5;
203
+ author.book_set.valuesList = [firstBook, secondBook];
204
+
205
+ expect(author.serialize()).toEqual({
206
+ book_set: [21, 22],
207
+ id: 5,
208
+ name: "Grace Hopper",
209
+ age: 85,
210
+ });
211
+ });
212
+
213
+ test("syncModelToForm copies model values into matching form elements", () => {
214
+ const author = new Author({
215
+ id: 9,
216
+ name: "Margaret Hamilton",
217
+ age: 33,
218
+ });
219
+ const $form = $(`
220
+ <form class="${author.formId}">
221
+ <input name="name" value="" />
222
+ <input name="age" value="" />
223
+ </form>
224
+ `).data("model_pk", 9);
225
+
226
+ $("body").append($form);
227
+ author.syncModelToForm($form);
228
+
229
+ expect($form.find("[name=name]").val()).toBe("Margaret Hamilton");
230
+ expect($form.find("[name=age]").val()).toBe("33");
231
+ });
232
+
233
+ test("syncForm parses hidden ids and file uploads into the model", () => {
234
+ const content = new BookContent();
235
+ const file = new File(["chapter"], "chapter.txt", { type: "text/plain" });
236
+ const $form = $(`
237
+ <form class="${content.formId}">
238
+ <input type="hidden" name="user" value="7" />
239
+ <input type="file" name="text" />
240
+ </form>
241
+ `).data("model_pk", -1);
242
+ const fileInput = $form.find("[name=text]")[0];
243
+
244
+ Object.defineProperty(fileInput, "files", {
245
+ configurable: true,
246
+ value: [file],
247
+ });
248
+
249
+ $("body").append($form);
250
+ const result = content.syncForm($form);
251
+
252
+ expect(result.user).toBe(7);
253
+ expect(result.text).toBe(file);
254
+ expect(content.user.id).toBe(7);
255
+ expect(content.text).toBe(file);
256
+ });
257
+
258
+ test("fixture validation still rejects invalid required values", () => {
259
+ const author = new Author();
260
+
261
+ expect(() => {
262
+ author.name = "";
263
+ }).toThrow("This field is required");
264
+ });
265
+
266
+ test("queryset load receives rows through the mocked socket receiver", async () => {
267
+ const books = new Author({ id: 2 }).book_set;
268
+
269
+ const result = await books.load();
270
+
271
+ expect(result).toMatchObject({
272
+ type: "load",
273
+ });
274
+ expect(books.getIds()).toEqual([21, 22]);
275
+ expect(books[0].title).toBe("Compiler Notes");
276
+ expect(books[1].author.id).toBe(2);
277
+ expect(books.socket.sentMessages.map((x) => x.event_type)).toEqual([
278
+ "connect",
279
+ "load",
280
+ ]);
281
+ });
282
+
283
+ test("queryset save sends model payloads and updates cached rows from the socket response", async () => {
284
+ const books = new Author({ id: 2 }).book_set;
285
+
286
+ await books.load({ author: 2 });
287
+ books[0].title = "Compiler Notes Revised";
288
+
289
+ const [response] = await books.save({ pk: 21 });
290
+
291
+ expect(response.data.instance).toHaveLength(1);
292
+ expect(response.data.instance[0].title).toBe("Compiler Notes Revised");
293
+ expect(books.byId(21).title).toBe("Compiler Notes Revised");
294
+ expect(books.socket.sentMessages.at(-1)).toMatchObject({
295
+ event_type: "save",
296
+ args: {
297
+ pk: 21,
298
+ data: {
299
+ id: 21,
300
+ title: "Compiler Notes Revised",
301
+ author: 2,
302
+ pk: 21,
303
+ },
304
+ },
305
+ });
306
+ });
307
+
308
+ test("queryset create uses the mocked socket receiver to append new rows", async () => {
309
+ const books = new Author({ id: 2 }).book_set;
310
+ const newBook = new Book({
311
+ title: "New Language Manual",
312
+ });
313
+
314
+ newBook.author = 2;
315
+
316
+ const response = await books.create({ elem: newBook });
317
+
318
+ expect(response.data.instance.id).toBe(23);
319
+ expect(response.data.instance.title).toBe("New Language Manual");
320
+ expect(books.byId(23).author.id).toBe(2);
321
+ expect(books.socket.sentMessages.at(-1)).toMatchObject({
322
+ event_type: "create",
323
+ args: {
324
+ data: {
325
+ id: null,
326
+ title: "New Language Manual",
327
+ author: 2,
328
+ },
329
+ },
330
+ });
331
+ });
332
+ });
@@ -0,0 +1,145 @@
1
+ import {SdcModel, SdcQuerySet} from '../../src/index.js';
2
+
3
+ export default class Author extends SdcModel {
4
+
5
+
6
+ static fields = {
7
+ "book_set": {
8
+ "type": "ForeignKey",
9
+ "required": false,
10
+ "max_length": null,
11
+ "is_relation": true,
12
+ "many_to_many": false,
13
+ "one_to_many": true,
14
+ "many_to_one": false,
15
+ "one_to_one": false,
16
+ "related_model": "Book",
17
+ "remote_field": "author"
18
+ },
19
+ "id": {
20
+ "type": "BigAutoField",
21
+ "required": false,
22
+ "max_length": null,
23
+ "is_relation": false,
24
+ "many_to_many": null,
25
+ "one_to_many": null,
26
+ "many_to_one": null,
27
+ "one_to_one": null,
28
+ "related_model": null,
29
+ "remote_field": null
30
+ },
31
+ "name": {
32
+ "type": "CharField",
33
+ "required": true,
34
+ "max_length": 255,
35
+ "is_relation": false,
36
+ "many_to_many": null,
37
+ "one_to_many": null,
38
+ "many_to_one": null,
39
+ "one_to_one": null,
40
+ "related_model": null,
41
+ "remote_field": null
42
+ },
43
+ "age": {
44
+ "type": "IntegerField",
45
+ "required": true,
46
+ "max_length": null,
47
+ "is_relation": false,
48
+ "many_to_many": null,
49
+ "one_to_many": null,
50
+ "many_to_one": null,
51
+ "one_to_one": null,
52
+ "related_model": null,
53
+ "remote_field": null
54
+ }
55
+ }
56
+
57
+ constructor(data = {}) {
58
+ super("Author");
59
+ this._toManyFields = [];
60
+ this._book_set = new SdcQuerySet('Book');
61
+ this._toManyFields.push([this._book_set, 'author']);
62
+ this._id = null;
63
+ this._name = null;
64
+ this._age = null;
65
+ this.setValues(data);
66
+ }
67
+
68
+ setValues(data = {}) {
69
+ data.id ??= data.pk ?? null;
70
+ try {
71
+ this.book_set.setFilter({ author: data.id });
72
+ this.book_set = data.book_set || [];
73
+ } catch {}
74
+ try {
75
+ this.id = data.id ?? null;
76
+ } catch {}
77
+ try {
78
+ this.name = data.name ?? null;
79
+ } catch {}
80
+ try {
81
+ this.age = data.age ?? null;
82
+ } catch {}
83
+ }
84
+
85
+ set book_set(value){
86
+ this.setbook_set(value);
87
+ this._updateForm('book_set');
88
+ }
89
+
90
+ set id(value){
91
+ this.setid(value);
92
+ this._updateForm('id');
93
+ }
94
+
95
+ set name(value){
96
+ this.setname(value);
97
+ this._updateForm('name');
98
+ }
99
+
100
+ set age(value){
101
+ this.setage(value);
102
+ this._updateForm('age');
103
+ }
104
+
105
+
106
+ setbook_set(value){
107
+ this.validate(value, Author.fields.book_set);
108
+ const a = this.parseValue(value, Author.fields.book_set)
109
+ this._book_set.setIds(this.parseValue(value, Author.fields.book_set));
110
+ }
111
+
112
+ setid(value){
113
+ this.validate(value, Author.fields.id);
114
+ this._toManyFields.forEach(([x, fn]) => x.setFilter({[fn]: value}));
115
+ this._id = this.parseValue(value, Author.fields.id);
116
+ }
117
+
118
+ setname(value){
119
+ this.validate(value, Author.fields.name);
120
+ this._name = this.parseValue(value, Author.fields.name);
121
+ }
122
+
123
+ setage(value){
124
+ this.validate(value, Author.fields.age);
125
+ this._age = this.parseValue(value, Author.fields.age);
126
+ }
127
+
128
+
129
+ get book_set(){
130
+ return this._book_set;
131
+ }
132
+
133
+ get id(){
134
+ return this._id;
135
+ }
136
+
137
+ get name(){
138
+ return this._name;
139
+ }
140
+
141
+ get age(){
142
+ return this._age;
143
+ }
144
+
145
+ }
@@ -0,0 +1,112 @@
1
+ import {SdcModel, SdcQuerySet} from '../../src/index.js';
2
+
3
+ export default class Book extends SdcModel {
4
+
5
+
6
+ static fields = {
7
+ "id": {
8
+ "type": "BigAutoField",
9
+ "required": false,
10
+ "max_length": null,
11
+ "is_relation": false,
12
+ "many_to_many": null,
13
+ "one_to_many": null,
14
+ "many_to_one": null,
15
+ "one_to_one": null,
16
+ "related_model": null,
17
+ "remote_field": null
18
+ },
19
+ "title": {
20
+ "type": "CharField",
21
+ "required": true,
22
+ "max_length": 255,
23
+ "is_relation": false,
24
+ "many_to_many": null,
25
+ "one_to_many": null,
26
+ "many_to_one": null,
27
+ "one_to_one": null,
28
+ "related_model": null,
29
+ "remote_field": null
30
+ },
31
+ "author": {
32
+ "type": "ForeignKey",
33
+ "required": true,
34
+ "max_length": null,
35
+ "is_relation": true,
36
+ "many_to_many": false,
37
+ "one_to_many": false,
38
+ "many_to_one": true,
39
+ "one_to_one": false,
40
+ "related_model": "Author",
41
+ "remote_field": "book"
42
+ }
43
+ }
44
+
45
+ constructor(data = {}) {
46
+ super("Book");
47
+ this._toManyFields = [];
48
+ this._id = null;
49
+ this._title = null;
50
+ this._author = new SdcQuerySet('Author');
51
+ this.setValues(data);
52
+ }
53
+
54
+ setValues(data = {}) {
55
+ data.id ??= data.pk ?? null;
56
+ try {
57
+ this.id = data.id ?? null;
58
+ } catch {}
59
+ try {
60
+ this.title = data.title ?? null;
61
+ } catch {}
62
+ try {
63
+ if (data.author) { this.author = data.author; }
64
+ } catch {}
65
+ }
66
+
67
+ set id(value){
68
+ this.setid(value);
69
+ this._updateForm('id');
70
+ }
71
+
72
+ set title(value){
73
+ this.settitle(value);
74
+ this._updateForm('title');
75
+ }
76
+
77
+ set author(value){
78
+ this.setauthor(value);
79
+ this._updateForm('author');
80
+ }
81
+
82
+
83
+ setid(value){
84
+ this.validate(value, Book.fields.id);
85
+ this._toManyFields.forEach(([x, fn]) => x.setFilter({[fn]: value}));
86
+ this._id = this.parseValue(value, Book.fields.id);
87
+ }
88
+
89
+ settitle(value){
90
+ this.validate(value, Book.fields.title);
91
+ this._title = this.parseValue(value, Book.fields.title);
92
+ }
93
+
94
+ setauthor(value){
95
+ this.validate(value, Book.fields.author);
96
+ this._author.setIds(this.parseValue(value, Book.fields.author));
97
+ }
98
+
99
+
100
+ get id(){
101
+ return this._id;
102
+ }
103
+
104
+ get title(){
105
+ return this._title;
106
+ }
107
+
108
+ get author(){
109
+ return this._author.length > 0 ? this._author[0] : this._author.new();
110
+ }
111
+
112
+ }
@@ -0,0 +1,114 @@
1
+ import {SdcModel, SdcQuerySet} from '../../src/index.js';
2
+
3
+ export default class BookContent extends SdcModel {
4
+
5
+
6
+ static fields = {
7
+ "id": {
8
+ "type": "BigAutoField",
9
+ "required": false,
10
+ "max_length": null,
11
+ "is_relation": false,
12
+ "many_to_many": null,
13
+ "one_to_many": null,
14
+ "many_to_one": null,
15
+ "one_to_one": null,
16
+ "related_model": null,
17
+ "remote_field": null
18
+ },
19
+ "user": {
20
+ "type": "ForeignKey",
21
+ "required": false,
22
+ "max_length": null,
23
+ "is_relation": true,
24
+ "many_to_many": false,
25
+ "one_to_many": false,
26
+ "many_to_one": true,
27
+ "one_to_one": false,
28
+ "related_model": "SdcUser",
29
+ "remote_field": "bookcontent"
30
+ },
31
+ "text": {
32
+ "type": "FileField",
33
+ "required": true,
34
+ "max_length": 100,
35
+ "is_relation": false,
36
+ "many_to_many": null,
37
+ "one_to_many": null,
38
+ "many_to_one": null,
39
+ "one_to_one": null,
40
+ "related_model": null,
41
+ "remote_field": null,
42
+ "max_size": 5368709120,
43
+ "allowed_types": null
44
+ }
45
+ }
46
+
47
+ constructor(data = {}) {
48
+ super("BookContent");
49
+ this._toManyFields = [];
50
+ this._id = null;
51
+ this._user = new SdcQuerySet('SdcUser');
52
+ this._text = null;
53
+ this.setValues(data);
54
+ }
55
+
56
+ setValues(data = {}) {
57
+ data.id ??= data.pk ?? null;
58
+ try {
59
+ this.id = data.id ?? null;
60
+ } catch {}
61
+ try {
62
+ if (data.user) { this.user = data.user; }
63
+ } catch {}
64
+ try {
65
+ this.text = data.text ?? null;
66
+ } catch {}
67
+ }
68
+
69
+ set id(value){
70
+ this.setid(value);
71
+ this._updateForm('id');
72
+ }
73
+
74
+ set user(value){
75
+ this.setuser(value);
76
+ this._updateForm('user');
77
+ }
78
+
79
+ set text(value){
80
+ this.settext(value);
81
+ this._updateForm('text');
82
+ }
83
+
84
+
85
+ setid(value){
86
+ this.validate(value, BookContent.fields.id);
87
+ this._toManyFields.forEach(([x, fn]) => x.setFilter({[fn]: value}));
88
+ this._id = this.parseValue(value, BookContent.fields.id);
89
+ }
90
+
91
+ setuser(value){
92
+ this.validate(value, BookContent.fields.user);
93
+ this._user.setIds(this.parseValue(value, BookContent.fields.user));
94
+ }
95
+
96
+ settext(value){
97
+ this.validate(value, BookContent.fields.text);
98
+ this._text = this.parseValue(value, BookContent.fields.text);
99
+ }
100
+
101
+
102
+ get id(){
103
+ return this._id;
104
+ }
105
+
106
+ get user(){
107
+ return this._user.length > 0 ? this._user[0] : this._user.new();
108
+ }
109
+
110
+ get text(){
111
+ return this._text;
112
+ }
113
+
114
+ }