velocious 1.0.577 → 1.0.578

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 (42) hide show
  1. package/README.md +11 -5
  2. package/build/background-jobs/main.js +2 -2
  3. package/build/configuration-types.js +1 -0
  4. package/build/configuration.js +52 -10
  5. package/build/database/drivers/mysql/structure-sql.js +70 -9
  6. package/build/database/query/with-count.js +8 -8
  7. package/build/database/record/record-not-found-error.js +1 -0
  8. package/build/frontend-model-controller.js +71 -69
  9. package/build/frontend-models/base.js +10 -1
  10. package/build/src/background-jobs/main.js +3 -3
  11. package/build/src/configuration-types.d.ts +5 -0
  12. package/build/src/configuration-types.d.ts.map +1 -1
  13. package/build/src/configuration-types.js +2 -1
  14. package/build/src/configuration.d.ts +13 -1
  15. package/build/src/configuration.d.ts.map +1 -1
  16. package/build/src/configuration.js +52 -11
  17. package/build/src/database/drivers/mysql/structure-sql.d.ts +11 -0
  18. package/build/src/database/drivers/mysql/structure-sql.d.ts.map +1 -1
  19. package/build/src/database/drivers/mysql/structure-sql.js +61 -10
  20. package/build/src/database/query/with-count.js +8 -8
  21. package/build/src/database/record/record-not-found-error.d.ts +1 -0
  22. package/build/src/database/record/record-not-found-error.d.ts.map +1 -1
  23. package/build/src/database/record/record-not-found-error.js +2 -1
  24. package/build/src/frontend-model-controller.d.ts +10 -10
  25. package/build/src/frontend-model-controller.d.ts.map +1 -1
  26. package/build/src/frontend-model-controller.js +73 -67
  27. package/build/src/frontend-models/base.d.ts.map +1 -1
  28. package/build/src/frontend-models/base.js +11 -2
  29. package/build/src/velocious-error.d.ts +10 -0
  30. package/build/src/velocious-error.d.ts.map +1 -1
  31. package/build/src/velocious-error.js +8 -2
  32. package/build/velocious-error.js +7 -1
  33. package/package.json +2 -2
  34. package/src/background-jobs/main.js +2 -2
  35. package/src/configuration-types.js +1 -0
  36. package/src/configuration.js +52 -10
  37. package/src/database/drivers/mysql/structure-sql.js +70 -9
  38. package/src/database/query/with-count.js +8 -8
  39. package/src/database/record/record-not-found-error.js +1 -0
  40. package/src/frontend-model-controller.js +71 -69
  41. package/src/frontend-models/base.js +10 -1
  42. package/src/velocious-error.js +7 -1
@@ -6,11 +6,15 @@ export default class VelociousError extends Error {
6
6
  * @param {object} [args] - Options.
7
7
  * @param {?} [args.cause] - Error cause.
8
8
  * @param {string} [args.code] - Optional error code.
9
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
10
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
9
11
  * @returns {VelociousError} - Client-safe error instance.
10
12
  */
11
13
  static safe(message: string, args?: {
12
14
  cause?: unknown;
13
15
  code?: string | undefined;
16
+ details?: import("./configuration-types.js").ClientErrorPayloadReporterPayload | undefined;
17
+ errorType?: "application_error" | "authorization_error" | "record_not_found" | "validation_error" | undefined;
14
18
  }): VelociousError;
15
19
  /**
16
20
  * Runs constructor.
@@ -18,14 +22,20 @@ export default class VelociousError extends Error {
18
22
  * @param {object} [args] - Options.
19
23
  * @param {?} [args.cause] - Error cause.
20
24
  * @param {string} [args.code] - Optional error code.
25
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
26
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
21
27
  * @param {boolean} [args.safeToExpose] - Whether the message is safe to return to clients.
22
28
  */
23
29
  constructor(message: string, args?: {
24
30
  cause?: unknown;
25
31
  code?: string | undefined;
32
+ details?: import("./configuration-types.js").ClientErrorPayloadReporterPayload | undefined;
33
+ errorType?: "application_error" | "authorization_error" | "record_not_found" | "validation_error" | undefined;
26
34
  safeToExpose?: boolean | undefined;
27
35
  });
28
36
  code: string | undefined;
37
+ details: import("./configuration-types.js").ClientErrorPayloadReporterPayload | undefined;
38
+ errorType: "application_error" | "authorization_error" | "record_not_found" | "validation_error" | undefined;
29
39
  safeToExpose: boolean;
30
40
  }
31
41
  //# sourceMappingURL=velocious-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"velocious-error.d.ts","sourceRoot":"","sources":["../../src/velocious-error.js"],"names":[],"mappings":"AAEA,uEAAuE;AACvE;IAmBE;;;;;;;OAOG;IACH,qBANW,MAAM,SAEd;QAAiB,KAAK,GAAd,OAAC;QACa,IAAI;KAC1B,GAAU,cAAc,CAI1B;IA5BD;;;;;;;OAOG;IACH,qBANW,MAAM,SAEd;QAAiB,KAAK,GAAd,OAAC;QACa,IAAI;QACH,YAAY;KACrC,EASA;IAFC,yBAAgB;IAChB,sBAAgC;CAcnC"}
1
+ {"version":3,"file":"velocious-error.d.ts","sourceRoot":"","sources":["../../src/velocious-error.js"],"names":[],"mappings":"AAEA,uEAAuE;AACvE;IAuBE;;;;;;;;;OASG;IACH,qBARW,MAAM,SAEd;QAAiB,KAAK,GAAd,OAAC;QACa,IAAI;QAC0D,OAAO;QACU,SAAS;KAC9G,GAAU,cAAc,CAI1B;IAlCD;;;;;;;;;OASG;IACH,qBARW,MAAM,SAEd;QAAiB,KAAK,GAAd,OAAC;QACa,IAAI;QAC0D,OAAO;QACU,SAAS;QACvF,YAAY;KACrC,EAWA;IAJC,yBAAgB;IAChB,0FAAsB;IACtB,6GAA0B;IAC1B,sBAAgC;CAgBnC"}
@@ -7,13 +7,17 @@ export default class VelociousError extends Error {
7
7
  * @param {object} [args] - Options.
8
8
  * @param {?} [args.cause] - Error cause.
9
9
  * @param {string} [args.code] - Optional error code.
10
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
11
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
10
12
  * @param {boolean} [args.safeToExpose] - Whether the message is safe to return to clients.
11
13
  */
12
14
  constructor(message, args = {}) {
13
- const { cause, code, safeToExpose = false } = args;
15
+ const { cause, code, details, errorType, safeToExpose = false } = args;
14
16
  super(message, { cause });
15
17
  this.name = "VelociousError";
16
18
  this.code = code;
19
+ this.details = details;
20
+ this.errorType = errorType;
17
21
  this.safeToExpose = safeToExpose;
18
22
  }
19
23
  /**
@@ -22,10 +26,12 @@ export default class VelociousError extends Error {
22
26
  * @param {object} [args] - Options.
23
27
  * @param {?} [args.cause] - Error cause.
24
28
  * @param {string} [args.code] - Optional error code.
29
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
30
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
25
31
  * @returns {VelociousError} - Client-safe error instance.
26
32
  */
27
33
  static safe(message, args = {}) {
28
34
  return new VelociousError(message, { ...args, safeToExpose: true });
29
35
  }
30
36
  }
31
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVsb2Npb3VzLWVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3ZlbG9jaW91cy1lcnJvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosdUVBQXVFO0FBQ3ZFLE1BQU0sQ0FBQyxPQUFPLE9BQU8sY0FBZSxTQUFRLEtBQUs7SUFDL0M7Ozs7Ozs7T0FPRztJQUNILFlBQVksT0FBTyxFQUFFLElBQUksR0FBRyxFQUFFO1FBQzVCLE1BQU0sRUFBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLFlBQVksR0FBRyxLQUFLLEVBQUMsR0FBRyxJQUFJLENBQUE7UUFFaEQsS0FBSyxDQUFDLE9BQU8sRUFBRSxFQUFDLEtBQUssRUFBQyxDQUFDLENBQUE7UUFFdkIsSUFBSSxDQUFDLElBQUksR0FBRyxnQkFBZ0IsQ0FBQTtRQUM1QixJQUFJLENBQUMsSUFBSSxHQUFHLElBQUksQ0FBQTtRQUNoQixJQUFJLENBQUMsWUFBWSxHQUFHLFlBQVksQ0FBQTtJQUNsQyxDQUFDO0lBRUQ7Ozs7Ozs7T0FPRztJQUNILE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksR0FBRyxFQUFFO1FBQzVCLE9BQU8sSUFBSSxjQUFjLENBQUMsT0FBTyxFQUFFLEVBQUMsR0FBRyxJQUFJLEVBQUUsWUFBWSxFQUFFLElBQUksRUFBQyxDQUFDLENBQUE7SUFDbkUsQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbi8qKiBGcmFtZXdvcmsgZXJyb3Igd2l0aCBvcHRpb25hbCBjbGllbnQtc2FmZSBtZXNzYWdlIGV4cG9zdXJlIGZsYWcuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBWZWxvY2lvdXNFcnJvciBleHRlbmRzIEVycm9yIHtcbiAgLyoqXG4gICAqIFJ1bnMgY29uc3RydWN0b3IuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlIC0gRXJyb3IgbWVzc2FnZS5cbiAgICogQHBhcmFtIHtvYmplY3R9IFthcmdzXSAtIE9wdGlvbnMuXG4gICAqIEBwYXJhbSB7P30gW2FyZ3MuY2F1c2VdIC0gRXJyb3IgY2F1c2UuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBbYXJncy5jb2RlXSAtIE9wdGlvbmFsIGVycm9yIGNvZGUuXG4gICAqIEBwYXJhbSB7Ym9vbGVhbn0gW2FyZ3Muc2FmZVRvRXhwb3NlXSAtIFdoZXRoZXIgdGhlIG1lc3NhZ2UgaXMgc2FmZSB0byByZXR1cm4gdG8gY2xpZW50cy5cbiAgICovXG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2UsIGFyZ3MgPSB7fSkge1xuICAgIGNvbnN0IHtjYXVzZSwgY29kZSwgc2FmZVRvRXhwb3NlID0gZmFsc2V9ID0gYXJnc1xuXG4gICAgc3VwZXIobWVzc2FnZSwge2NhdXNlfSlcblxuICAgIHRoaXMubmFtZSA9IFwiVmVsb2Npb3VzRXJyb3JcIlxuICAgIHRoaXMuY29kZSA9IGNvZGVcbiAgICB0aGlzLnNhZmVUb0V4cG9zZSA9IHNhZmVUb0V4cG9zZVxuICB9XG5cbiAgLyoqXG4gICAqIFJ1bnMgc2FmZS5cbiAgICogQHBhcmFtIHtzdHJpbmd9IG1lc3NhZ2UgLSBFcnJvciBtZXNzYWdlLlxuICAgKiBAcGFyYW0ge29iamVjdH0gW2FyZ3NdIC0gT3B0aW9ucy5cbiAgICogQHBhcmFtIHs/fSBbYXJncy5jYXVzZV0gLSBFcnJvciBjYXVzZS5cbiAgICogQHBhcmFtIHtzdHJpbmd9IFthcmdzLmNvZGVdIC0gT3B0aW9uYWwgZXJyb3IgY29kZS5cbiAgICogQHJldHVybnMge1ZlbG9jaW91c0Vycm9yfSAtIENsaWVudC1zYWZlIGVycm9yIGluc3RhbmNlLlxuICAgKi9cbiAgc3RhdGljIHNhZmUobWVzc2FnZSwgYXJncyA9IHt9KSB7XG4gICAgcmV0dXJuIG5ldyBWZWxvY2lvdXNFcnJvcihtZXNzYWdlLCB7Li4uYXJncywgc2FmZVRvRXhwb3NlOiB0cnVlfSlcbiAgfVxufVxuIl19
37
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmVsb2Npb3VzLWVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3ZlbG9jaW91cy1lcnJvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosdUVBQXVFO0FBQ3ZFLE1BQU0sQ0FBQyxPQUFPLE9BQU8sY0FBZSxTQUFRLEtBQUs7SUFDL0M7Ozs7Ozs7OztPQVNHO0lBQ0gsWUFBWSxPQUFPLEVBQUUsSUFBSSxHQUFHLEVBQUU7UUFDNUIsTUFBTSxFQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxZQUFZLEdBQUcsS0FBSyxFQUFDLEdBQUcsSUFBSSxDQUFBO1FBRXBFLEtBQUssQ0FBQyxPQUFPLEVBQUUsRUFBQyxLQUFLLEVBQUMsQ0FBQyxDQUFBO1FBRXZCLElBQUksQ0FBQyxJQUFJLEdBQUcsZ0JBQWdCLENBQUE7UUFDNUIsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUE7UUFDaEIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUE7UUFDdEIsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUE7UUFDMUIsSUFBSSxDQUFDLFlBQVksR0FBRyxZQUFZLENBQUE7SUFDbEMsQ0FBQztJQUVEOzs7Ozs7Ozs7T0FTRztJQUNILE1BQU0sQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksR0FBRyxFQUFFO1FBQzVCLE9BQU8sSUFBSSxjQUFjLENBQUMsT0FBTyxFQUFFLEVBQUMsR0FBRyxJQUFJLEVBQUUsWUFBWSxFQUFFLElBQUksRUFBQyxDQUFDLENBQUE7SUFDbkUsQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbi8qKiBGcmFtZXdvcmsgZXJyb3Igd2l0aCBvcHRpb25hbCBjbGllbnQtc2FmZSBtZXNzYWdlIGV4cG9zdXJlIGZsYWcuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBWZWxvY2lvdXNFcnJvciBleHRlbmRzIEVycm9yIHtcbiAgLyoqXG4gICAqIFJ1bnMgY29uc3RydWN0b3IuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlIC0gRXJyb3IgbWVzc2FnZS5cbiAgICogQHBhcmFtIHtvYmplY3R9IFthcmdzXSAtIE9wdGlvbnMuXG4gICAqIEBwYXJhbSB7P30gW2FyZ3MuY2F1c2VdIC0gRXJyb3IgY2F1c2UuXG4gICAqIEBwYXJhbSB7c3RyaW5nfSBbYXJncy5jb2RlXSAtIE9wdGlvbmFsIGVycm9yIGNvZGUuXG4gICAqIEBwYXJhbSB7aW1wb3J0KFwiLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLkNsaWVudEVycm9yUGF5bG9hZFJlcG9ydGVyUGF5bG9hZH0gW2FyZ3MuZGV0YWlsc10gLSBTdHJ1Y3R1cmVkIGNsaWVudC1zYWZlIGVycm9yIGRldGFpbHMuXG4gICAqIEBwYXJhbSB7XCJhcHBsaWNhdGlvbl9lcnJvclwiIHwgXCJhdXRob3JpemF0aW9uX2Vycm9yXCIgfCBcInJlY29yZF9ub3RfZm91bmRcIiB8IFwidmFsaWRhdGlvbl9lcnJvclwifSBbYXJncy5lcnJvclR5cGVdIC0gU3RhYmxlIGNsaWVudC1mYWNpbmcgZXJyb3IgY2F0ZWdvcnkuXG4gICAqIEBwYXJhbSB7Ym9vbGVhbn0gW2FyZ3Muc2FmZVRvRXhwb3NlXSAtIFdoZXRoZXIgdGhlIG1lc3NhZ2UgaXMgc2FmZSB0byByZXR1cm4gdG8gY2xpZW50cy5cbiAgICovXG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2UsIGFyZ3MgPSB7fSkge1xuICAgIGNvbnN0IHtjYXVzZSwgY29kZSwgZGV0YWlscywgZXJyb3JUeXBlLCBzYWZlVG9FeHBvc2UgPSBmYWxzZX0gPSBhcmdzXG5cbiAgICBzdXBlcihtZXNzYWdlLCB7Y2F1c2V9KVxuXG4gICAgdGhpcy5uYW1lID0gXCJWZWxvY2lvdXNFcnJvclwiXG4gICAgdGhpcy5jb2RlID0gY29kZVxuICAgIHRoaXMuZGV0YWlscyA9IGRldGFpbHNcbiAgICB0aGlzLmVycm9yVHlwZSA9IGVycm9yVHlwZVxuICAgIHRoaXMuc2FmZVRvRXhwb3NlID0gc2FmZVRvRXhwb3NlXG4gIH1cblxuICAvKipcbiAgICogUnVucyBzYWZlLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gbWVzc2FnZSAtIEVycm9yIG1lc3NhZ2UuXG4gICAqIEBwYXJhbSB7b2JqZWN0fSBbYXJnc10gLSBPcHRpb25zLlxuICAgKiBAcGFyYW0gez99IFthcmdzLmNhdXNlXSAtIEVycm9yIGNhdXNlLlxuICAgKiBAcGFyYW0ge3N0cmluZ30gW2FyZ3MuY29kZV0gLSBPcHRpb25hbCBlcnJvciBjb2RlLlxuICAgKiBAcGFyYW0ge2ltcG9ydChcIi4vY29uZmlndXJhdGlvbi10eXBlcy5qc1wiKS5DbGllbnRFcnJvclBheWxvYWRSZXBvcnRlclBheWxvYWR9IFthcmdzLmRldGFpbHNdIC0gU3RydWN0dXJlZCBjbGllbnQtc2FmZSBlcnJvciBkZXRhaWxzLlxuICAgKiBAcGFyYW0ge1wiYXBwbGljYXRpb25fZXJyb3JcIiB8IFwiYXV0aG9yaXphdGlvbl9lcnJvclwiIHwgXCJyZWNvcmRfbm90X2ZvdW5kXCIgfCBcInZhbGlkYXRpb25fZXJyb3JcIn0gW2FyZ3MuZXJyb3JUeXBlXSAtIFN0YWJsZSBjbGllbnQtZmFjaW5nIGVycm9yIGNhdGVnb3J5LlxuICAgKiBAcmV0dXJucyB7VmVsb2Npb3VzRXJyb3J9IC0gQ2xpZW50LXNhZmUgZXJyb3IgaW5zdGFuY2UuXG4gICAqL1xuICBzdGF0aWMgc2FmZShtZXNzYWdlLCBhcmdzID0ge30pIHtcbiAgICByZXR1cm4gbmV3IFZlbG9jaW91c0Vycm9yKG1lc3NhZ2UsIHsuLi5hcmdzLCBzYWZlVG9FeHBvc2U6IHRydWV9KVxuICB9XG59XG4iXX0=
@@ -8,15 +8,19 @@ export default class VelociousError extends Error {
8
8
  * @param {object} [args] - Options.
9
9
  * @param {?} [args.cause] - Error cause.
10
10
  * @param {string} [args.code] - Optional error code.
11
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
12
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
11
13
  * @param {boolean} [args.safeToExpose] - Whether the message is safe to return to clients.
12
14
  */
13
15
  constructor(message, args = {}) {
14
- const {cause, code, safeToExpose = false} = args
16
+ const {cause, code, details, errorType, safeToExpose = false} = args
15
17
 
16
18
  super(message, {cause})
17
19
 
18
20
  this.name = "VelociousError"
19
21
  this.code = code
22
+ this.details = details
23
+ this.errorType = errorType
20
24
  this.safeToExpose = safeToExpose
21
25
  }
22
26
 
@@ -26,6 +30,8 @@ export default class VelociousError extends Error {
26
30
  * @param {object} [args] - Options.
27
31
  * @param {?} [args.cause] - Error cause.
28
32
  * @param {string} [args.code] - Optional error code.
33
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [args.details] - Structured client-safe error details.
34
+ * @param {"application_error" | "authorization_error" | "record_not_found" | "validation_error"} [args.errorType] - Stable client-facing error category.
29
35
  * @returns {VelociousError} - Client-safe error instance.
30
36
  */
31
37
  static safe(message, args = {}) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.577",
6
+ "version": "1.0.578",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -111,7 +111,7 @@
111
111
  "eslint-plugin-jsdoc": "^63.0.0",
112
112
  "eslint-plugin-jsdoc-inline-type-casts": "https://github.com/kaspernj/eslint-plugin-jsdoc-inline-type-casts/archive/refs/heads/master.tar.gz",
113
113
  "eslint-plugin-jsdoc-tag-lines": "^1.0.1",
114
- "fallow": "3.10.0",
114
+ "fallow": "3.11.0",
115
115
  "globals": "^17.0.0",
116
116
  "mssql": "^12.0.0",
117
117
  "mysql": "^2.18.1",
@@ -786,9 +786,9 @@ export default class BackgroundJobsMain {
786
786
  options: message.options || {}
787
787
  })
788
788
 
789
- jsonSocket.send({type: "schedule-replaced", ...result})
790
789
  this._notifyEnqueued()
791
790
  await this._drain()
791
+ jsonSocket.send({type: "schedule-replaced", ...result})
792
792
  } catch (error) {
793
793
  this._handleClientMutationError({
794
794
  context: {jobName: message.jobName, scheduleKey: message.scheduleKey, stage: "background-job-replace-scheduled"},
@@ -812,9 +812,9 @@ export default class BackgroundJobsMain {
812
812
  try {
813
813
  const result = await this.store.cancelScheduled(message.scheduleKey)
814
814
 
815
- jsonSocket.send({type: "schedule-cancelled", ...result})
816
815
  this._notifyEnqueued()
817
816
  await this._drain()
817
+ jsonSocket.send({type: "schedule-cancelled", ...result})
818
818
  } catch (error) {
819
819
  this._handleClientMutationError({
820
820
  context: {scheduleKey: message.scheduleKey, stage: "background-job-cancel-scheduled"},
@@ -312,6 +312,7 @@
312
312
  * @property {string} controller - Controller class name.
313
313
  * @property {string} [action] - Controller action or endpoint label.
314
314
  * @property {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [commandType] - Frontend-model command type.
315
+ * @property {string} [correlationId] - Server-generated identifier shared by an unexpected client error and framework reports.
315
316
  * @property {boolean} [expectedError] - Whether the error is an expected user-flow failure.
316
317
  * @property {boolean} [frontendModelEndpoint] - Whether the error came from the frontend-model endpoint.
317
318
  * @property {string} [model] - Frontend-model name from the failed request.
@@ -286,6 +286,19 @@ export default class VelociousConfiguration {
286
286
  }
287
287
 
288
288
  this._isInitialized = false
289
+ this._modelsInitialized = false
290
+ /**
291
+ * Invalidates model phases that started before database connections closed.
292
+ * @type {number}
293
+ */
294
+ this._modelInitializationGeneration = 0
295
+ /**
296
+ * In-progress `initializeModels()` promise. Model initialization is an
297
+ * atomic bootstrap phase: concurrent callers share it, and a rejection
298
+ * leaves the phase eligible for a later complete attempt.
299
+ * @type {Promise<void> | undefined}
300
+ */
301
+ this._initializeModelsPromise = undefined
289
302
  /**
290
303
  * In-progress `initialize()` promise, memoized so concurrent callers await
291
304
  * the same bootstrap. Reset to undefined if initialization fails.
@@ -2020,25 +2033,39 @@ export default class VelociousConfiguration {
2020
2033
  * @returns {Promise<void>} - Resolves when complete.
2021
2034
  */
2022
2035
  async initializeModels(args = {type: "server"}) {
2023
- if (!this._modelsInitialized) {
2024
- this._modelsInitialized = true
2036
+ if (this._modelsInitialized) return
2037
+ if (this._initializeModelsPromise) return await this._initializeModelsPromise
2025
2038
 
2039
+ const modelInitializationGeneration = this._modelInitializationGeneration
2040
+ const initializeModelsPromise = (async () => {
2026
2041
  const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
2027
2042
  && process.env.VELOCIOUS_BROWSER_TESTS === "true"
2028
2043
  && this.getEnvironment() === "test"
2029
2044
 
2030
- if (shouldSkipDummyModelInitialization) {
2031
- return
2045
+ if (!shouldSkipDummyModelInitialization) {
2046
+ if (this._initializeModels) {
2047
+ await this._initializeModels({configuration: this, type: args.type})
2048
+ }
2049
+
2050
+ await this.getEnvironmentHandler().initializePackageModels(this)
2051
+ await initializeAuditedModelRelationships(this)
2052
+
2053
+ await this.getEnvironmentHandler().initializeFrontendModelWebsocketPublishers(this)
2032
2054
  }
2033
2055
 
2034
- if (this._initializeModels) {
2035
- await this._initializeModels({configuration: this, type: args.type})
2056
+ if (this._modelInitializationGeneration === modelInitializationGeneration) {
2057
+ this._modelsInitialized = true
2036
2058
  }
2059
+ })()
2037
2060
 
2038
- await this.getEnvironmentHandler().initializePackageModels(this)
2039
- await initializeAuditedModelRelationships(this)
2061
+ this._initializeModelsPromise = initializeModelsPromise
2040
2062
 
2041
- await this.getEnvironmentHandler().initializeFrontendModelWebsocketPublishers(this)
2063
+ try {
2064
+ await initializeModelsPromise
2065
+ } finally {
2066
+ if (this._initializeModelsPromise === initializeModelsPromise) {
2067
+ this._initializeModelsPromise = undefined
2068
+ }
2042
2069
  }
2043
2070
  }
2044
2071
 
@@ -2073,6 +2100,12 @@ export default class VelociousConfiguration {
2073
2100
 
2074
2101
  this._initializePromise = (async () => {
2075
2102
  await this.initializeModels({type})
2103
+
2104
+ // Model initialization can be invalidated by a concurrent connection close.
2105
+ // If models are not ready, stop without marking the configuration initialized
2106
+ // so the next caller retries a full bootstrap.
2107
+ if (!this._modelsInitialized) return
2108
+
2076
2109
  await this.getEnvironmentHandler().autoDiscoverResources(this)
2077
2110
  this._mergeDiscoveredAbilityResources()
2078
2111
  this._validateResourceRelationshipsOnModels()
@@ -2104,6 +2137,13 @@ export default class VelociousConfiguration {
2104
2137
  this._initializePromise = undefined
2105
2138
  throw error
2106
2139
  }
2140
+
2141
+ // If the inner IIFE returned without marking the configuration initialized
2142
+ // (e.g. because models were invalidated mid-bootstrap), clear the promise so
2143
+ // a later call retries a full bootstrap.
2144
+ if (!this._isInitialized) {
2145
+ this._initializePromise = undefined
2146
+ }
2107
2147
  }
2108
2148
 
2109
2149
  /**
@@ -3244,8 +3284,10 @@ export default class VelociousConfiguration {
3244
3284
  PoolClass.clearGlobalConnections(this)
3245
3285
  }
3246
3286
 
3247
- // Allow models to be re-initialized after connections are closed.
3287
+ // Allow full re-initialization after connections are closed.
3288
+ this._modelInitializationGeneration += 1
3248
3289
  this._modelsInitialized = false
3290
+ this._isInitialized = false
3249
3291
  }
3250
3292
  })()
3251
3293
 
@@ -20,6 +20,9 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
20
20
  const {driver} = this
21
21
  const isMariaDb = await this._isMariaDb()
22
22
  const rows = await driver.query("SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_type, table_name")
23
+ const foreignKeyRows = await driver.query("SELECT table_name, referenced_table_name FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND referenced_table_schema = DATABASE() AND referenced_table_name IS NOT NULL")
24
+ const baseTableNames = []
25
+ const views = []
23
26
  const statements = []
24
27
 
25
28
  for (const row of rows) {
@@ -31,24 +34,82 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
31
34
  if (!tableName || !tableType) continue
32
35
 
33
36
  if (tableType == "BASE TABLE") {
34
- const createRows = await driver.query(`SHOW CREATE TABLE ${driver.quoteTable(tableName)}`)
35
- const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
36
- const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
37
-
38
- if (createStatement) statements.push(normalizeSqlStatement(createStatement))
37
+ baseTableNames.push(tableName)
39
38
  } else if (tableType == "VIEW" || (isMariaDb && tableType == "SYSTEM VIEW")) {
40
- const createRows = await driver.query(`SHOW CREATE VIEW ${driver.quoteTable(tableName)}`)
41
- const createStatement = this._mysqlCreateStatement(createRows?.[0])
42
-
43
- if (createStatement) statements.push(normalizeSqlStatement(createStatement))
39
+ views.push(tableName)
44
40
  }
45
41
  }
46
42
 
43
+ for (const tableName of this._orderBaseTables({foreignKeyRows, tableNames: baseTableNames})) {
44
+ const createRows = await driver.query(`SHOW CREATE TABLE ${driver.quoteTable(tableName)}`)
45
+ const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
46
+ const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
47
+
48
+ if (createStatement) statements.push(normalizeSqlStatement(createStatement))
49
+ }
50
+
51
+ for (const tableName of views) {
52
+ const createRows = await driver.query(`SHOW CREATE VIEW ${driver.quoteTable(tableName)}`)
53
+ const createStatement = this._mysqlCreateStatement(createRows?.[0])
54
+
55
+ if (createStatement) statements.push(normalizeSqlStatement(createStatement))
56
+ }
57
+
47
58
  if (statements.length == 0) return null
48
59
 
49
60
  return `${statements.join("\n\n")}\n`
50
61
  }
51
62
 
63
+ /**
64
+ * Orders tables so referenced tables are created before their dependents.
65
+ * @param {object} args - Options object.
66
+ * @param {Array<Record<string, ?>>} args.foreignKeyRows - Foreign key metadata rows.
67
+ * @param {string[]} args.tableNames - Base table names in their existing order.
68
+ * @returns {string[]} - Ordered table names.
69
+ */
70
+ _orderBaseTables({foreignKeyRows, tableNames}) {
71
+ const pendingTableNames = new Set(tableNames)
72
+ /** @type {Record<string, Set<string>>} */
73
+ const dependenciesByTableName = {}
74
+ const orderedTableNames = []
75
+
76
+ for (const tableName of tableNames) {
77
+ dependenciesByTableName[tableName] = new Set()
78
+ }
79
+
80
+ for (const row of foreignKeyRows) {
81
+ const tableNameValue = row.table_name || row.TABLE_NAME
82
+ const referencedTableNameValue = row.referenced_table_name || row.REFERENCED_TABLE_NAME
83
+ const tableName = tableNameValue ? String(tableNameValue) : ""
84
+ const referencedTableName = referencedTableNameValue ? String(referencedTableNameValue) : ""
85
+
86
+ if (tableName == referencedTableName || !pendingTableNames.has(tableName) || !pendingTableNames.has(referencedTableName)) continue
87
+
88
+ dependenciesByTableName[tableName].add(referencedTableName)
89
+ }
90
+
91
+ while (pendingTableNames.size > 0) {
92
+ const nextTableName = tableNames.find((tableName) => {
93
+ if (!pendingTableNames.has(tableName)) return false
94
+
95
+ return Array.from(dependenciesByTableName[tableName]).every((dependencyTableName) => !pendingTableNames.has(dependencyTableName))
96
+ })
97
+
98
+ if (!nextTableName) {
99
+ for (const tableName of tableNames) {
100
+ if (pendingTableNames.has(tableName)) orderedTableNames.push(tableName)
101
+ }
102
+
103
+ break
104
+ }
105
+
106
+ orderedTableNames.push(nextTableName)
107
+ pendingTableNames.delete(nextTableName)
108
+ }
109
+
110
+ return orderedTableNames
111
+ }
112
+
52
113
  /**
53
114
  * Runs is maria db.
54
115
  * @returns {Promise<boolean>} - Resolves with Whether maria db.
@@ -161,22 +161,22 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
161
161
 
162
162
  const foreignKey = relationship.getForeignKey()
163
163
  /**
164
- * Where conditions.
164
+ * Mandatory cohort conditions.
165
165
  * @type {Record<string, ?>} */
166
- const whereConditions = {[foreignKey]: parentIds}
166
+ const mandatoryWhereConditions = {[foreignKey]: parentIds}
167
167
 
168
168
  if (relationship.getPolymorphic && relationship.getPolymorphic()) {
169
169
  const typeColumn = relationship.getPolymorphicTypeColumn()
170
- whereConditions[typeColumn] = modelClass.getModelName()
171
- }
172
-
173
- if (entry.where) {
174
- Object.assign(whereConditions, entry.where)
170
+ mandatoryWhereConditions[typeColumn] = modelClass.getModelName()
175
171
  }
176
172
 
177
173
  const baseQuery = sourceModel.queryForModel(targetModelClass)
178
174
  baseQuery._forceQualifyBaseTable = true
179
- baseQuery.where(whereConditions)
175
+ baseQuery.where(mandatoryWhereConditions)
176
+
177
+ if (entry.where) {
178
+ baseQuery.where(entry.where)
179
+ }
180
180
 
181
181
  const countQuery = relationship.applyScope(baseQuery)
182
182
 
@@ -1,3 +1,4 @@
1
1
  // @ts-check
2
2
 
3
+ /** Backend missing-record error whose diagnostic message stays server-side. */
3
4
  export default class RecordNotFoundError extends Error {}