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
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
 
3
+ import {randomUUID} from "node:crypto"
3
4
  import * as inflection from "inflection"
4
5
  import Controller from "./controller.js"
5
6
  import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
@@ -14,6 +15,7 @@ import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendMod
14
15
  import {requestDetails} from "./error-reporting/request-details.js"
15
16
  import RoutesResolver from "./routes/resolver.js"
16
17
  import {ValidationError} from "./database/record/index.js"
18
+ import RecordNotFoundError from "./database/record/record-not-found-error.js"
17
19
  import { normalizeDateStringForWrite } from "./database/datetime-storage.js"
18
20
  import VelociousError from "./velocious-error.js"
19
21
  import isDate from "./utils/is-date.js"
@@ -231,31 +233,18 @@ function frontendModelErrorHasVelociousMetadata(error) {
231
233
  return isPlainObject(errorRecord.velocious)
232
234
  }
233
235
 
234
- /**
235
- * Whether the error has a frontend-model error type marker.
236
- * @param {unknown} error - Caught error.
237
- * @returns {boolean} Whether the error has an error type.
238
- */
239
- function frontendModelErrorHasErrorType(error) {
240
- if (!error || typeof error !== "object") return false
241
-
242
- // Runtime checks above narrow this caught value to the marker record shape.
243
- const errorRecord = /** @type {{errorType?: string}} */ (error)
244
-
245
- return typeof errorRecord.errorType === "string" && errorRecord.errorType.length > 0
246
- }
247
-
248
236
  /**
249
237
  * Whether the error is an expected frontend-model user-flow failure.
250
238
  * @param {unknown} error - Caught error.
251
239
  * @returns {boolean} Whether the error is expected.
252
240
  */
253
241
  function frontendModelExpectedError(error) {
242
+ if (error instanceof RecordNotFoundError) return true
254
243
  if (error instanceof ValidationError) return true
255
244
  if (error instanceof VelociousError && error.safeToExpose) return true
256
245
  if (frontendModelErrorHasVelociousMetadata(error)) return true
257
246
 
258
- return frontendModelErrorHasErrorType(error)
247
+ return false
259
248
  }
260
249
 
261
250
  /**
@@ -285,6 +274,10 @@ function frontendModelVelociousMetadataForError(error) {
285
274
  * @returns {string} - Message safe to return to API clients.
286
275
  */
287
276
  function frontendModelClientMessageForError(error) {
277
+ if (error instanceof RecordNotFoundError) {
278
+ return "Record not found."
279
+ }
280
+
288
281
  if (error instanceof VelociousError && error.safeToExpose) {
289
282
  return error.message
290
283
  }
@@ -323,6 +316,10 @@ function frontendModelDebugPayloadForError({configuration, environment, error})
323
316
  return {}
324
317
  }
325
318
 
319
+ if (error instanceof RecordNotFoundError) {
320
+ return {}
321
+ }
322
+
326
323
  if (frontendModelErrorHasVelociousMetadata(error)) {
327
324
  return {}
328
325
  }
@@ -3098,11 +3095,16 @@ export default class FrontendModelController extends Controller {
3098
3095
  /**
3099
3096
  * Runs frontend model error payload.
3100
3097
  * @param {string} errorMessage - Error message.
3098
+ * @param {object} [options] - Structured error fields.
3099
+ * @param {import("./configuration-types.js").ClientErrorPayloadReporterPayload} [options.details] - Client-safe details.
3100
+ * @param {"application_error" | "authorization_error" | "internal_error" | "record_not_found" | "validation_error"} [options.errorType] - Stable client-facing error category.
3101
3101
  * @returns {Record<string, ?>} - Error payload.
3102
3102
  */
3103
- frontendModelErrorPayload(errorMessage) {
3103
+ frontendModelErrorPayload(errorMessage, options = {}) {
3104
3104
  return {
3105
+ ...(options.details ? {details: options.details} : {}),
3105
3106
  errorMessage,
3107
+ ...(options.errorType ? {errorType: options.errorType} : {}),
3106
3108
  status: "error"
3107
3109
  }
3108
3110
  }
@@ -3127,6 +3129,7 @@ export default class FrontendModelController extends Controller {
3127
3129
  */
3128
3130
  frontendModelEndpointErrorContext({action, commandType, error, model, requestId}) {
3129
3131
  let resolvedModel = model
3132
+ const expectedError = frontendModelExpectedError(error)
3130
3133
 
3131
3134
  if (!resolvedModel) {
3132
3135
  const cachedParams = this._frontendModelParamsOverride || this._frontendModelParams
@@ -3138,7 +3141,8 @@ export default class FrontendModelController extends Controller {
3138
3141
  action,
3139
3142
  commandType,
3140
3143
  controller: this.constructor.name,
3141
- expectedError: frontendModelExpectedError(error),
3144
+ ...(expectedError ? {} : {correlationId: randomUUID()}),
3145
+ expectedError,
3142
3146
  frontendModelEndpoint: true,
3143
3147
  model: resolvedModel,
3144
3148
  requestId
@@ -3154,6 +3158,22 @@ export default class FrontendModelController extends Controller {
3154
3158
  async frontendModelClientErrorPayloadForError(error, endpointErrorContext) {
3155
3159
  const velociousMetadata = frontendModelVelociousMetadataForError(error)
3156
3160
  const normalizedError = error instanceof Error ? error : new Error(String(error))
3161
+ /** @type {import("./configuration-types.js").ClientErrorPayloadReporterPayload} */
3162
+ const safeErrorPayload = {}
3163
+
3164
+ if (error instanceof VelociousError && error.safeToExpose) {
3165
+ if (error.errorType) safeErrorPayload.errorType = error.errorType
3166
+ if (error.details) safeErrorPayload.details = error.details
3167
+ } else if (error instanceof RecordNotFoundError) {
3168
+ safeErrorPayload.errorType = "record_not_found"
3169
+ } else if (velociousMetadata) {
3170
+ if (typeof velociousMetadata.errorType === "string") {
3171
+ safeErrorPayload.errorType = velociousMetadata.errorType
3172
+ }
3173
+ if (isPlainObject(velociousMetadata.details)) {
3174
+ safeErrorPayload.details = velociousMetadata.details
3175
+ }
3176
+ }
3157
3177
 
3158
3178
  let validationErrorsPayload = {}
3159
3179
 
@@ -3179,7 +3199,14 @@ export default class FrontendModelController extends Controller {
3179
3199
  }
3180
3200
  }
3181
3201
 
3202
+ const reporterPayload = await this.getConfiguration().clientErrorPayloadForError({
3203
+ context: endpointErrorContext || {controller: this.constructor.name},
3204
+ error: normalizedError,
3205
+ request: this.getRequest()
3206
+ })
3207
+
3182
3208
  return {
3209
+ ...reporterPayload,
3183
3210
  ...this.frontendModelErrorPayload(frontendModelClientMessageForError(error)),
3184
3211
  ...frontendModelDebugPayloadForError({
3185
3212
  configuration: this.getConfiguration(),
@@ -3187,28 +3214,22 @@ export default class FrontendModelController extends Controller {
3187
3214
  error
3188
3215
  }),
3189
3216
  ...(velociousMetadata ? {velocious: velociousMetadata} : {}),
3217
+ ...safeErrorPayload,
3190
3218
  ...validationErrorsPayload,
3191
- ...(await this.getConfiguration().clientErrorPayloadForError({
3192
- context: endpointErrorContext || {controller: this.constructor.name},
3193
- error: normalizedError,
3194
- request: this.getRequest()
3195
- }))
3219
+ ...(!endpointErrorContext?.expectedError && endpointErrorContext?.correlationId
3220
+ ? {correlationId: endpointErrorContext.correlationId, errorType: "internal_error"}
3221
+ : {})
3196
3222
  }
3197
3223
  }
3198
3224
 
3199
3225
  /**
3200
3226
  * Runs frontend model log endpoint error.
3201
3227
  * @param {object} args - Error log args.
3202
- * @param {string} args.action - Endpoint/action label.
3203
3228
  * @param {?} args.error - Caught error.
3204
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
3205
- * @param {string | undefined} [args.model] - Request model name when available.
3206
- * @param {string | undefined} [args.requestId] - Batch request id when available.
3229
+ * @param {FrontendModelEndpointErrorContext} args.errorContext - Shared client/logging error context.
3207
3230
  * @returns {Promise<void>} - Resolves after logging.
3208
3231
  */
3209
- async frontendModelLogEndpointError({action, error, commandType, model, requestId}) {
3210
- const errorContext = this.frontendModelEndpointErrorContext({action, commandType, error, model, requestId})
3211
-
3232
+ async frontendModelLogEndpointError({error, errorContext}) {
3212
3233
  // Expected user-flow errors are surfaced to clients by
3213
3234
  // frontendModelClientErrorPayloadForError, but skipped here so monitoring
3214
3235
  // stays focused on real backend failures.
@@ -3219,11 +3240,12 @@ export default class FrontendModelController extends Controller {
3219
3240
  : String(error)
3220
3241
 
3221
3242
  await this.logger.error(() => ["Frontend model endpoint request failed", {
3222
- action,
3223
- commandType,
3243
+ action: errorContext.action,
3244
+ commandType: errorContext.commandType,
3245
+ correlationId: errorContext.correlationId,
3224
3246
  error: errorMessage,
3225
3247
  model: errorContext.model,
3226
- requestId
3248
+ requestId: errorContext.requestId
3227
3249
  }])
3228
3250
 
3229
3251
  // Surface genuinely unexpected backend failures on the framework-error
@@ -3231,6 +3253,7 @@ export default class FrontendModelController extends Controller {
3231
3253
  // controller silently swallowing them behind the generic "Request
3232
3254
  // failed." client message.
3233
3255
  const errorPayload = {
3256
+ correlationId: errorContext.correlationId,
3234
3257
  context: errorContext,
3235
3258
  error: error instanceof Error ? error : new Error(String(error)),
3236
3259
  request: this.getRequest(),
@@ -3257,7 +3280,7 @@ export default class FrontendModelController extends Controller {
3257
3280
  } catch (error) {
3258
3281
  const errorContext = this.frontendModelEndpointErrorContext({action, commandType: action, error})
3259
3282
 
3260
- await this.frontendModelLogEndpointError({action, commandType: action, error, model: errorContext.model})
3283
+ await this.frontendModelLogEndpointError({error, errorContext})
3261
3284
 
3262
3285
  await this.render({
3263
3286
  json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
@@ -3334,7 +3357,7 @@ export default class FrontendModelController extends Controller {
3334
3357
  )
3335
3358
 
3336
3359
  if (!model) {
3337
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3360
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3338
3361
  }
3339
3362
 
3340
3363
  const serializedModel = await resource.serialize(model, "create")
@@ -3343,7 +3366,7 @@ export default class FrontendModelController extends Controller {
3343
3366
  }
3344
3367
 
3345
3368
  if ((typeof id !== "string" && typeof id !== "number") || `${id}`.length < 1) {
3346
- return this.frontendModelErrorPayload("Expected model id.")
3369
+ return this.frontendModelErrorPayload("Expected model id.", {errorType: "validation_error"})
3347
3370
  }
3348
3371
 
3349
3372
  if (action === "attach") {
@@ -3361,7 +3384,7 @@ export default class FrontendModelController extends Controller {
3361
3384
  const model = await this.frontendModelFindRecord("attach", id)
3362
3385
 
3363
3386
  if (!model) {
3364
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3387
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3365
3388
  }
3366
3389
 
3367
3390
  await model.getAttachmentByName(attachmentName).attach(attachmentInput)
@@ -3377,13 +3400,13 @@ export default class FrontendModelController extends Controller {
3377
3400
  const model = await this.frontendModelFindRecord("download", id)
3378
3401
 
3379
3402
  if (!model) {
3380
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3403
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3381
3404
  }
3382
3405
 
3383
3406
  const downloadedAttachment = await model.getAttachmentByName(attachmentParams.attachmentName).download(attachmentParams.attachmentId)
3384
3407
 
3385
3408
  if (!downloadedAttachment) {
3386
- return this.frontendModelErrorPayload("Attachment not found.")
3409
+ return this.frontendModelErrorPayload("Attachment not found.", {errorType: "record_not_found"})
3387
3410
  }
3388
3411
 
3389
3412
  return {
@@ -3406,7 +3429,7 @@ export default class FrontendModelController extends Controller {
3406
3429
  const model = await this.frontendModelFindRecord("url", id)
3407
3430
 
3408
3431
  if (!model) {
3409
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3432
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3410
3433
  }
3411
3434
 
3412
3435
  const url = await model.getAttachmentByName(attachmentParams.attachmentName).url(attachmentParams.attachmentId)
@@ -3428,7 +3451,7 @@ export default class FrontendModelController extends Controller {
3428
3451
  const model = await this.frontendModelFindRecord("attachmentList", id)
3429
3452
 
3430
3453
  if (!model) {
3431
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3454
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3432
3455
  }
3433
3456
 
3434
3457
  const attachments = await model.getAttachmentByName(attachmentParams.attachmentName).listMetadata()
@@ -3443,7 +3466,7 @@ export default class FrontendModelController extends Controller {
3443
3466
  const model = await this.frontendModelFindRecord("find", id)
3444
3467
 
3445
3468
  if (!model) {
3446
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3469
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3447
3470
  }
3448
3471
 
3449
3472
  await this.frontendModelComputeAbilities([model])
@@ -3459,7 +3482,7 @@ export default class FrontendModelController extends Controller {
3459
3482
  const model = await this.frontendModelFindRecord("update", id)
3460
3483
 
3461
3484
  if (!model) {
3462
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3485
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3463
3486
  }
3464
3487
 
3465
3488
  const updatedModel = await resource.update(model, mutationAttributes.attributes, {
@@ -3475,7 +3498,7 @@ export default class FrontendModelController extends Controller {
3475
3498
  const model = await this.frontendModelFindRecord("destroy", id)
3476
3499
 
3477
3500
  if (!model) {
3478
- return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3501
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`, {errorType: "record_not_found"})
3479
3502
  }
3480
3503
 
3481
3504
  await resource.destroy(model)
@@ -3624,12 +3647,7 @@ export default class FrontendModelController extends Controller {
3624
3647
  : undefined
3625
3648
  })
3626
3649
 
3627
- await this.frontendModelLogEndpointError({
3628
- action: errorContext.action,
3629
- commandType: errorContext.commandType,
3630
- error,
3631
- model: errorContext.model
3632
- })
3650
+ await this.frontendModelLogEndpointError({error, errorContext})
3633
3651
 
3634
3652
  results.push({
3635
3653
  idempotencyKey,
@@ -3724,12 +3742,7 @@ export default class FrontendModelController extends Controller {
3724
3742
  model: mutation.model
3725
3743
  })
3726
3744
 
3727
- await this.frontendModelLogEndpointError({
3728
- action: errorContext.action,
3729
- commandType: errorContext.commandType,
3730
- error,
3731
- model: errorContext.model
3732
- })
3745
+ await this.frontendModelLogEndpointError({error, errorContext})
3733
3746
 
3734
3747
  return {
3735
3748
  response: await this.frontendModelClientErrorPayloadForError(error, errorContext),
@@ -3754,12 +3767,7 @@ export default class FrontendModelController extends Controller {
3754
3767
  model: mutation.model
3755
3768
  })
3756
3769
 
3757
- await this.frontendModelLogEndpointError({
3758
- action: errorContext.action,
3759
- commandType: errorContext.commandType,
3760
- error,
3761
- model: errorContext.model
3762
- })
3770
+ await this.frontendModelLogEndpointError({error, errorContext})
3763
3771
 
3764
3772
  return {
3765
3773
  response,
@@ -4275,13 +4283,7 @@ export default class FrontendModelController extends Controller {
4275
4283
  requestId
4276
4284
  })
4277
4285
 
4278
- await this.frontendModelLogEndpointError({
4279
- action: errorContext.action,
4280
- commandType: errorContext.commandType,
4281
- error,
4282
- model: errorContext.model,
4283
- requestId: errorContext.requestId
4284
- })
4286
+ await this.frontendModelLogEndpointError({error, errorContext})
4285
4287
 
4286
4288
  responses.push({
4287
4289
  requestId,
@@ -4506,7 +4508,7 @@ export default class FrontendModelController extends Controller {
4506
4508
  } catch (error) {
4507
4509
  const errorContext = this.frontendModelEndpointErrorContext({action: "frontendCustomCommand", commandType: "custom-command", error})
4508
4510
 
4509
- await this.frontendModelLogEndpointError({action: errorContext.action, commandType: errorContext.commandType, error, model: errorContext.model})
4511
+ await this.frontendModelLogEndpointError({error, errorContext})
4510
4512
 
4511
4513
  await this.render({
4512
4514
  json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
@@ -4744,7 +4744,10 @@ export default class FrontendModelBase {
4744
4744
  ? response.errorMessage
4745
4745
  : `Request failed for ${this.name}#${commandType}`)
4746
4746
 
4747
- const error = /** @type {Error & {velocious?: Record<string, ?>, errorType?: string, validationErrors?: Record<string, ?>, debugErrorClass?: string, debugBacktrace?: string[]}} */ (new Error(errorMessage))
4747
+ const error = /** @type {Error & {correlationId?: string, details?: Record<string, ?>, errorMessage?: string, velocious?: Record<string, ?>, errorType?: string, validationErrors?: Record<string, ?>, debugErrorClass?: string, debugBacktrace?: string[]}} */ (new Error(errorMessage))
4748
+ if (hasErrorMessage) {
4749
+ error.errorMessage = response.errorMessage
4750
+ }
4748
4751
  if (response.velocious && typeof response.velocious === "object") {
4749
4752
  error.velocious = response.velocious
4750
4753
  }
@@ -4754,6 +4757,12 @@ export default class FrontendModelBase {
4754
4757
  if (response.validationErrors && typeof response.validationErrors === "object") {
4755
4758
  error.validationErrors = response.validationErrors
4756
4759
  }
4760
+ if (response.details && typeof response.details === "object") {
4761
+ error.details = response.details
4762
+ }
4763
+ if (typeof response.correlationId === "string") {
4764
+ error.correlationId = response.correlationId
4765
+ }
4757
4766
  // Forward server-provided debug detail (included only when the backend
4758
4767
  // deems the requester allowed to see it, e.g. an admin) so callers can
4759
4768
  // render the real error class and stack trace instead of the generic