@tellescope/sdk 1.255.18 → 1.256.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.
- package/lib/cjs/sdk.d.ts +7 -2
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js +99 -8
- package/lib/cjs/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/default_summary_journey.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js +278 -0
- package/lib/cjs/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js +203 -72
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +212 -33
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
- package/lib/cjs/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js +102 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +242 -0
- package/lib/cjs/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +443 -0
- package/lib/cjs/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +231 -0
- package/lib/cjs/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +1003 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +215 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1200 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2330 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js +204 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +489 -255
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +0 -1
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +9 -5
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/session.d.ts +0 -1
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/esm/tests/api_tests/custom_dashboards.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/custom_dashboards.test.js +99 -8
- package/lib/esm/tests/api_tests/custom_dashboards.test.js.map +1 -1
- package/lib/esm/tests/api_tests/default_summary_journey.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/default_summary_journey.test.js.map +1 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.js +248 -0
- package/lib/esm/tests/api_tests/file_download_unicode_names.test.js.map +1 -0
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js +205 -74
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +213 -34
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js +85 -9
- package/lib/esm/tests/api_tests/phone_calls_cold_transfer.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js +98 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js +238 -0
- package/lib/esm/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js +436 -0
- package/lib/esm/tests/api_tests/security/F-0015-webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts +35 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js +224 -0
- package/lib/esm/tests/api_tests/security/F-0025-proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +996 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +211 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1193 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2323 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js +200 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +489 -255
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +4 -0
- package/src/tests/api_tests/auto_merge_form_submission.test.ts +13 -4
- package/src/tests/api_tests/custom_dashboards.test.ts +92 -0
- package/src/tests/api_tests/file_download_unicode_names.test.ts +223 -0
- package/src/tests/api_tests/healthie_multi_integration.test.ts +103 -5
- package/src/tests/api_tests/openloop_webhooks.test.ts +138 -0
- package/src/tests/api_tests/phone_calls_cold_transfer.test.ts +54 -0
- package/src/tests/api_tests/phone_calls_conference_hold.test.ts +62 -0
- package/src/tests/api_tests/scoped_api_keys.test.ts +5 -0
- package/src/tests/api_tests/security/F-0013-sanitize-user-html-with-iframes.test.ts +215 -0
- package/src/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.ts +38 -4
- package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +721 -0
- package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +125 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +753 -0
- package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +1707 -0
- package/src/tests/api_tests/tickets_bulk_assign_care_team.test.ts +132 -0
- package/src/tests/tests.ts +232 -2
- package/test_generated.pdf +0 -0
- package/.env +0 -13
package/src/sdk.ts
CHANGED
|
@@ -716,6 +716,9 @@ type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
|
|
|
716
716
|
remove_conference_attendees: (args: extractFields<CustomActions['phone_calls']['remove_conference_attendees']['parameters']>) => (
|
|
717
717
|
Promise<extractFields<CustomActions['phone_calls']['remove_conference_attendees']['returns']>>
|
|
718
718
|
),
|
|
719
|
+
modify_conference_attendee_status: (args: extractFields<CustomActions['phone_calls']['modify_conference_attendee_status']['parameters']>) => (
|
|
720
|
+
Promise<extractFields<CustomActions['phone_calls']['modify_conference_attendee_status']['returns']>>
|
|
721
|
+
),
|
|
719
722
|
end_conference: (args: extractFields<CustomActions['phone_calls']['end_conference']['parameters']>) => (
|
|
720
723
|
Promise<extractFields<CustomActions['phone_calls']['end_conference']['returns']>>
|
|
721
724
|
),
|
|
@@ -1163,6 +1166,7 @@ export class Session extends SessionManager {
|
|
|
1163
1166
|
queries.phone_calls.cold_transfer = a => this._POST(`/v1${schema.phone_calls.customActions.cold_transfer.path}`, a)
|
|
1164
1167
|
queries.phone_calls.add_conference_attendees = a => this._POST(`/v1${schema.phone_calls.customActions.add_conference_attendees.path}`, a)
|
|
1165
1168
|
queries.phone_calls.remove_conference_attendees = a => this._POST(`/v1${schema.phone_calls.customActions.remove_conference_attendees.path}`, a)
|
|
1169
|
+
queries.phone_calls.modify_conference_attendee_status = a => this._POST(`/v1${schema.phone_calls.customActions.modify_conference_attendee_status.path}`, a)
|
|
1166
1170
|
queries.phone_calls.end_conference = a => this._POST(`/v1${schema.phone_calls.customActions.end_conference.path}`, a)
|
|
1167
1171
|
queries.phone_calls.cancel_recording = a => this._POST(`/v1${schema.phone_calls.customActions.cancel_recording.path}`, a)
|
|
1168
1172
|
queries.phone_calls.delete_recordings = a => this._POST(`/v1${schema.phone_calls.customActions.delete_recordings.path}`, a)
|
|
@@ -547,9 +547,9 @@ export const auto_merge_form_submission_tests = async ({ sdk, sdkNonAdmin }: { s
|
|
|
547
547
|
{ expectedResult: true }
|
|
548
548
|
)
|
|
549
549
|
|
|
550
|
-
// Test 12: Intake fields update destination enduser directly
|
|
550
|
+
// Test 12: Intake fields update destination enduser directly — except the authentication channels
|
|
551
551
|
await async_test(
|
|
552
|
-
"Auto-merge: Intake fields update destination enduser directly",
|
|
552
|
+
"Auto-merge: Intake fields update destination enduser directly (email/phone locked)",
|
|
553
553
|
async () => {
|
|
554
554
|
const testEmail = `automerge.intake.${Date.now()}@test.com`
|
|
555
555
|
const destination = await sdk.api.endusers.createOne({
|
|
@@ -579,10 +579,19 @@ export const auto_merge_form_submission_tests = async ({ sdk, sdkNonAdmin }: { s
|
|
|
579
579
|
await sdk.api.forms.deleteOne(form.id)
|
|
580
580
|
await sdk.api.endusers.deleteOne(destination.id)
|
|
581
581
|
|
|
582
|
-
//
|
|
582
|
+
// Non-auth intake fields are still applied to the destination — dateOfBirth landing here is
|
|
583
|
+
// also what proves the intake write ran at all, so the negative assertions below aren't vacuous.
|
|
583
584
|
return sourceDeleted
|
|
584
|
-
&& updatedDestination.phone === newPhone
|
|
585
585
|
&& updatedDestination.dateOfBirth === newDOB
|
|
586
|
+
// ...but email and phone are NOT applied. The merge attaches this response to a contact that
|
|
587
|
+
// existed before the submission, which sets FormResponse.authFieldIntakeDisabled, and these two
|
|
588
|
+
// are authentication channels (password reset resolves by email; OTPs are delivered to phone).
|
|
589
|
+
// Filling a BLANK phone is deliberately blocked too — contact-less patients are the primary
|
|
590
|
+
// takeover target, so "fill only if empty" would not close the hole. Staff update these fields.
|
|
591
|
+
// Previously this test asserted `phone === newPhone`; that behaviour was the vulnerability.
|
|
592
|
+
// See api_tests/security/public_endpoint_auth_field_lock.test.ts (G26).
|
|
593
|
+
&& !updatedDestination.phone
|
|
594
|
+
&& updatedDestination.email === testEmail
|
|
586
595
|
},
|
|
587
596
|
{ expectedResult: true }
|
|
588
597
|
)
|
|
@@ -408,8 +408,100 @@ export const custom_dashboards_tests = async ({ sdk, sdkNonAdmin }: { sdk: Sessi
|
|
|
408
408
|
{ shouldError: true, onError: () => true }
|
|
409
409
|
)
|
|
410
410
|
|
|
411
|
+
// Test 10: Dashboard mutations stream through /v1/data-sync
|
|
412
|
+
const syncFrom = new Date()
|
|
413
|
+
const dashboard_sync_records = (results: { modelName: string, recordId: string, data: string }[], recordId: string) => (
|
|
414
|
+
results.filter(r => r.modelName === 'custom_dashboards' && r.recordId === recordId)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
const syncedDashboard = await sdk.api.custom_dashboards.createOne({
|
|
418
|
+
title: "Sync Create Dashboard",
|
|
419
|
+
blocks: [{ type: "Inbox", colSpan: 12 }],
|
|
420
|
+
})
|
|
421
|
+
await wait(undefined, 100) // sync_record is not awaited by the mutation handler
|
|
422
|
+
|
|
423
|
+
await async_test(
|
|
424
|
+
"Dashboard create streams to sync, admin",
|
|
425
|
+
() => sdk.sync({ from: syncFrom }),
|
|
426
|
+
{
|
|
427
|
+
onResult: ({ results }) => {
|
|
428
|
+
const records = dashboard_sync_records(results, syncedDashboard.id)
|
|
429
|
+
return (
|
|
430
|
+
records.length === 1
|
|
431
|
+
&& records[0].data !== 'deleted'
|
|
432
|
+
&& JSON.parse(records[0].data).title === "Sync Create Dashboard"
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
await sdk.api.custom_dashboards.updateOne(syncedDashboard.id, { title: "Sync Update Dashboard" })
|
|
439
|
+
await wait(undefined, 100)
|
|
440
|
+
|
|
441
|
+
await async_test(
|
|
442
|
+
"Dashboard update streams to sync, admin",
|
|
443
|
+
() => sdk.sync({ from: syncFrom }),
|
|
444
|
+
{
|
|
445
|
+
onResult: ({ results }) => {
|
|
446
|
+
const records = dashboard_sync_records(results, syncedDashboard.id)
|
|
447
|
+
return (
|
|
448
|
+
records.length === 1
|
|
449
|
+
&& records[0].data !== 'deleted'
|
|
450
|
+
&& JSON.parse(records[0].data).title === "Sync Update Dashboard"
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
await sdk.api.custom_dashboards.deleteOne(syncedDashboard.id)
|
|
457
|
+
await wait(undefined, 100)
|
|
458
|
+
|
|
459
|
+
await async_test(
|
|
460
|
+
"Dashboard delete streams to sync, admin",
|
|
461
|
+
() => sdk.sync({ from: syncFrom }),
|
|
462
|
+
{
|
|
463
|
+
onResult: ({ results }) => {
|
|
464
|
+
const records = dashboard_sync_records(results, syncedDashboard.id)
|
|
465
|
+
return records.length === 1 && records[0].data === 'deleted'
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
// Restricted user (read: Default) only receives org-wide and assigned dashboards
|
|
471
|
+
const restrictedSyncFrom = new Date()
|
|
472
|
+
|
|
473
|
+
// re-save each fixture so it lands in the restricted user's sync window
|
|
474
|
+
await Promise.all([
|
|
475
|
+
sdk.api.custom_dashboards.updateOne(orgWideDashboard.id, { description: "org-wide sync" }),
|
|
476
|
+
sdk.api.custom_dashboards.updateOne(assignedDashboard.id, { description: "assigned sync" }),
|
|
477
|
+
sdk.api.custom_dashboards.updateOne(hiddenDashboard.id, { description: "hidden sync" }),
|
|
478
|
+
sdk.api.custom_dashboards.updateOne(explicitFalseDashboard.id, { description: "explicit false sync" }),
|
|
479
|
+
])
|
|
480
|
+
await wait(undefined, 100)
|
|
481
|
+
|
|
482
|
+
await async_test(
|
|
483
|
+
"Restricted user syncs org-wide and assigned dashboards only",
|
|
484
|
+
() => sdkRestricted.sync({ from: restrictedSyncFrom }),
|
|
485
|
+
{
|
|
486
|
+
onResult: ({ results }) => (
|
|
487
|
+
dashboard_sync_records(results, orgWideDashboard.id).length === 1
|
|
488
|
+
&& dashboard_sync_records(results, assignedDashboard.id).length === 1
|
|
489
|
+
&& dashboard_sync_records(results, hiddenDashboard.id).length === 0
|
|
490
|
+
&& dashboard_sync_records(results, explicitFalseDashboard.id).length === 0
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
|
|
411
495
|
// Toggling the flag on takes effect for the restricted user
|
|
496
|
+
const toggleSyncFrom = new Date()
|
|
412
497
|
await sdk.api.custom_dashboards.updateOne(hiddenDashboard.id, { visibleToAllUsers: true })
|
|
498
|
+
await wait(undefined, 100)
|
|
499
|
+
|
|
500
|
+
await async_test(
|
|
501
|
+
"Restricted user syncs dashboard after visibleToAllUsers enabled",
|
|
502
|
+
() => sdkRestricted.sync({ from: toggleSyncFrom }),
|
|
503
|
+
{ onResult: ({ results }) => dashboard_sync_records(results, hiddenDashboard.id).length === 1 }
|
|
504
|
+
)
|
|
413
505
|
|
|
414
506
|
await async_test(
|
|
415
507
|
"Restricted user can read dashboard after visibleToAllUsers enabled",
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
import * as buffer from 'buffer'
|
|
5
|
+
import { Session } from "../../sdk"
|
|
6
|
+
import {
|
|
7
|
+
assert,
|
|
8
|
+
log_header,
|
|
9
|
+
} from "@tellescope/testing"
|
|
10
|
+
import { setup_tests } from "../setup"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A presigned download URL signs response-content-disposition, which S3 validates: it rejects any
|
|
16
|
+
* value containing a character above U+00FF with 400 InvalidArgument "Header value cannot be
|
|
17
|
+
* represented using ISO-8859-1", and one with unbalanced quoting with "open quoted span". Because
|
|
18
|
+
* the parameter is part of the signature it cannot be repaired after the fact, so before the fix a
|
|
19
|
+
* file named with an em dash or a decomposed accent got a 200 from our API and a permanent 400 from
|
|
20
|
+
* S3 on every fetch.
|
|
21
|
+
*
|
|
22
|
+
* This test is the end-to-end check: it uploads files with those names and actually fetches the
|
|
23
|
+
* returned URL, which is the only thing that exercises S3's validation.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Built from code points rather than written out, so that this file stays pure ASCII and so that a
|
|
27
|
+
// combining accent (which renders as a mark on the preceding character) is unambiguous in source.
|
|
28
|
+
const c = (n: number) => String.fromCharCode(n)
|
|
29
|
+
const EM_DASH = c(0x2014)
|
|
30
|
+
const EN_DASH = c(0x2013)
|
|
31
|
+
const RIGHT_SINGLE_Q = c(0x2019)
|
|
32
|
+
const COMBINING_ACUTE = c(0x0301)
|
|
33
|
+
const CJK_BAO_GAO = c(0x62a5) + c(0x544a)
|
|
34
|
+
|
|
35
|
+
const IS_ASCII_ONLY = /^[\x20-\x7e]*$/
|
|
36
|
+
|
|
37
|
+
// The first three are the real filenames from the bug report.
|
|
38
|
+
const TEST_NAMES: { name: string, label: string, expectExtValue: boolean }[] = [
|
|
39
|
+
{
|
|
40
|
+
name: `First Aid Classes Information${EM_DASH}NWLI.pdf`,
|
|
41
|
+
label: 'em dash (U+2014)',
|
|
42
|
+
expectExtValue: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: `CHA${RIGHT_SINGLE_Q}s Waitlist Portal ${EN_DASH} Apply.pdf`,
|
|
46
|
+
label: 'right single quote (U+2019) and en dash (U+2013)',
|
|
47
|
+
expectExtValue: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: `Ren${'e' + COMBINING_ACUTE} Test .pdf`,
|
|
51
|
+
label: 'decomposed accent (e + U+0301)',
|
|
52
|
+
expectExtValue: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: `He said "hi".pdf`,
|
|
56
|
+
label: 'a double quote, which S3 rejects as an open quoted span',
|
|
57
|
+
expectExtValue: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: `${CJK_BAO_GAO}.pdf`,
|
|
61
|
+
label: 'an entirely non-ASCII name, so the fallback cannot come from the name',
|
|
62
|
+
expectExtValue: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: `plain ascii control.pdf`,
|
|
66
|
+
label: 'control: a plain ASCII name that always worked',
|
|
67
|
+
expectExtValue: true, // the space means filename* is emitted, carrying the real spaces
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
export const file_download_unicode_names_tests = async ({ sdk }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
72
|
+
log_header("File Download URL with Unicode Filenames")
|
|
73
|
+
|
|
74
|
+
const fileIds: string[] = []
|
|
75
|
+
|
|
76
|
+
const uploadFile = async (name: string) => {
|
|
77
|
+
const buff = buffer.Buffer.from(`test file content for ${JSON.stringify(name)}`)
|
|
78
|
+
const { presignedUpload, file } = await sdk.api.files.prepare_file_upload({
|
|
79
|
+
name,
|
|
80
|
+
type: 'application/pdf',
|
|
81
|
+
size: buff.byteLength,
|
|
82
|
+
})
|
|
83
|
+
await sdk.UPLOAD(presignedUpload as any, buff)
|
|
84
|
+
await sdk.api.files.confirm_file_upload({ id: file.id })
|
|
85
|
+
fileIds.push(file.id)
|
|
86
|
+
return { file, contents: buff }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// validateStatus so that a failure reports S3's actual status and XML <Message>, rather than an
|
|
90
|
+
// opaque axios throw. That is what makes this test diagnostic instead of merely red.
|
|
91
|
+
const fetchURL = (url: string) => axios.get(url, {
|
|
92
|
+
responseType: 'arraybuffer',
|
|
93
|
+
validateStatus: () => true,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
for (const { name, label, expectExtValue } of TEST_NAMES) {
|
|
98
|
+
const { file, contents } = await uploadFile(name)
|
|
99
|
+
|
|
100
|
+
const { downloadURL, name: returnedName } = await sdk.api.files.file_download_URL({
|
|
101
|
+
secureName: file.secureName,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
assert(
|
|
105
|
+
returnedName === name,
|
|
106
|
+
`endpoint returned "${returnedName}" instead of the stored name for ${label}`,
|
|
107
|
+
`stored name round-trips for ${label}`,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const response = await fetchURL(downloadURL)
|
|
111
|
+
|
|
112
|
+
// the bug: our API returned 200, S3 returned 400 on every fetch of the URL it gave us
|
|
113
|
+
assert(
|
|
114
|
+
response.status === 200,
|
|
115
|
+
`S3 returned ${response.status} for ${label}: ${buffer.Buffer.from(response.data ?? '').toString().slice(0, 400)}`,
|
|
116
|
+
`S3 returns 200 for ${label}`,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert(
|
|
120
|
+
buffer.Buffer.from(response.data).toString() === contents.toString(),
|
|
121
|
+
`downloaded bytes did not match for ${label}`,
|
|
122
|
+
`downloaded bytes match for ${label}`,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const disposition = (response.headers['content-disposition'] ?? '') as string
|
|
126
|
+
|
|
127
|
+
assert(
|
|
128
|
+
IS_ASCII_ONLY.test(disposition),
|
|
129
|
+
`Content-Disposition was not ASCII-only for ${label}: ${disposition}`,
|
|
130
|
+
`Content-Disposition is ASCII-only for ${label}`,
|
|
131
|
+
)
|
|
132
|
+
assert(
|
|
133
|
+
disposition.startsWith('attachment;'),
|
|
134
|
+
`expected an attachment disposition for ${label}, got: ${disposition}`,
|
|
135
|
+
`disposition is attachment for ${label}`,
|
|
136
|
+
)
|
|
137
|
+
assert(
|
|
138
|
+
expectExtValue === disposition.includes(`filename*=UTF-8''`),
|
|
139
|
+
`filename* presence was wrong for ${label}: ${disposition}`,
|
|
140
|
+
`filename* ${expectExtValue ? 'present' : 'absent'} for ${label}`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ---------------------------------------------------------------- *
|
|
145
|
+
* The download URL is cached server-side. preferInBrowser changes
|
|
146
|
+
* whether a Content-Disposition is signed into the URL at all, so it
|
|
147
|
+
* has to be part of the cache key - otherwise whichever variant is
|
|
148
|
+
* requested first is served to everyone for ~99 minutes.
|
|
149
|
+
* ---------------------------------------------------------------- */
|
|
150
|
+
const { file: cacheFile, contents: cacheContents } = await uploadFile(
|
|
151
|
+
`Cache Test${EM_DASH}Both Variants.pdf`
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const asAttachment = await sdk.api.files.file_download_URL({ secureName: cacheFile.secureName })
|
|
155
|
+
const inBrowser = await sdk.api.files.file_download_URL({
|
|
156
|
+
secureName: cacheFile.secureName,
|
|
157
|
+
preferInBrowser: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
assert(
|
|
161
|
+
asAttachment.downloadURL !== inBrowser.downloadURL,
|
|
162
|
+
'preferInBrowser returned the cached attachment URL, so the cache key ignores it',
|
|
163
|
+
'preferInBrowser is cached separately from the attachment URL',
|
|
164
|
+
)
|
|
165
|
+
assert(
|
|
166
|
+
asAttachment.downloadURL.includes('response-content-disposition'),
|
|
167
|
+
'the default URL was signed without a content disposition',
|
|
168
|
+
'the default URL signs a content disposition',
|
|
169
|
+
)
|
|
170
|
+
assert(
|
|
171
|
+
!inBrowser.downloadURL.includes('response-content-disposition'),
|
|
172
|
+
'the preferInBrowser URL was signed with a content disposition',
|
|
173
|
+
'the preferInBrowser URL omits the content disposition',
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for (const [variant, { downloadURL }] of [['attachment', asAttachment], ['inBrowser', inBrowser]] as const) {
|
|
177
|
+
const response = await fetchURL(downloadURL)
|
|
178
|
+
assert(
|
|
179
|
+
response.status === 200,
|
|
180
|
+
`S3 returned ${response.status} for the ${variant} URL: ${buffer.Buffer.from(response.data ?? '').toString().slice(0, 400)}`,
|
|
181
|
+
`S3 returns 200 for the ${variant} URL`,
|
|
182
|
+
)
|
|
183
|
+
assert(
|
|
184
|
+
buffer.Buffer.from(response.data).toString() === cacheContents.toString(),
|
|
185
|
+
`downloaded bytes did not match for the ${variant} URL`,
|
|
186
|
+
`downloaded bytes match for the ${variant} URL`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// a repeat request with identical arguments must still hit the cache
|
|
191
|
+
const repeated = await sdk.api.files.file_download_URL({ secureName: cacheFile.secureName })
|
|
192
|
+
assert(
|
|
193
|
+
repeated.downloadURL === asAttachment.downloadURL,
|
|
194
|
+
'an identical repeat request did not hit the cache',
|
|
195
|
+
'an identical repeat request hits the cache',
|
|
196
|
+
)
|
|
197
|
+
} finally {
|
|
198
|
+
for (const id of fileIds) {
|
|
199
|
+
await sdk.api.files.deleteOne(id).catch(() => {})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (require.main === module) {
|
|
205
|
+
console.log(`Using API URL: ${host}`)
|
|
206
|
+
const sdk = new Session({ host })
|
|
207
|
+
const sdkNonAdmin = new Session({ host })
|
|
208
|
+
|
|
209
|
+
const runTests = async () => {
|
|
210
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
211
|
+
await file_download_unicode_names_tests({ sdk, sdkNonAdmin })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
runTests()
|
|
215
|
+
.then(() => {
|
|
216
|
+
console.log("File download unicode names test suite completed successfully")
|
|
217
|
+
process.exit(0)
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
console.error("File download unicode names test suite failed:", error)
|
|
221
|
+
process.exit(1)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
log_header,
|
|
9
9
|
wait,
|
|
10
10
|
} from "@tellescope/testing"
|
|
11
|
-
import { evaluate_conditional_logic_for_enduser_fields } from "@tellescope/utilities"
|
|
11
|
+
import { evaluate_conditional_logic_for_enduser_fields, objects_equivalent } from "@tellescope/utilities"
|
|
12
12
|
import { setup_tests } from "../setup"
|
|
13
|
-
import { BUILT_INS_FOR_SET_FIELDS, HEALTHIE_TITLE } from "@tellescope/constants"
|
|
13
|
+
import { BUILT_INS_FOR_SET_FIELDS, ENDUSER_FIELD_TYPES, EnduserField, HEALTHIE_TITLE } from "@tellescope/constants"
|
|
14
14
|
|
|
15
15
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
16
16
|
|
|
@@ -27,6 +27,12 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
|
|
|
27
27
|
let enduserId = ''
|
|
28
28
|
let formsortEnduserId = ''
|
|
29
29
|
let formId = ''
|
|
30
|
+
let hiddenFormId = ''
|
|
31
|
+
let multipleChoiceFormId = ''
|
|
32
|
+
let hiddenIntakeEnduserId = ''
|
|
33
|
+
let multipleChoiceIntakeEnduserId = ''
|
|
34
|
+
let numberIntakeEnduserId = ''
|
|
35
|
+
const formResponseIds: string[] = []
|
|
30
36
|
|
|
31
37
|
try {
|
|
32
38
|
// ── Set Fields support ────────────────────────────────────────────────────────────────────
|
|
@@ -236,6 +242,90 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
|
|
|
236
242
|
{ onResult: e => e.healthieIntegrationId === 'somewhere-else' },
|
|
237
243
|
)
|
|
238
244
|
|
|
245
|
+
// ── Form intake field: "Update Patient Profile" writes the top-level enduser column ──
|
|
246
|
+
const submitIntakeResponse = async (enduserId: string, formId: string, response: any) => {
|
|
247
|
+
const { accessCode, response: prepared } = await sdk.api.form_responses.prepare_form_response({ enduserId, formId })
|
|
248
|
+
formResponseIds.push(prepared.id)
|
|
249
|
+
await sdk.api.form_responses.submit_form_response({ accessCode, responses: [response] })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const hiddenForm = await sdk.api.forms.createOne({ title: "Multi Healthie Intake (Hidden Value)" })
|
|
253
|
+
hiddenFormId = hiddenForm.id
|
|
254
|
+
const hiddenField = await sdk.api.form_fields.createOne({
|
|
255
|
+
formId: hiddenForm.id,
|
|
256
|
+
title: "Healthie Integration",
|
|
257
|
+
type: 'Hidden Value',
|
|
258
|
+
intakeField: 'healthieIntegrationId',
|
|
259
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const hiddenIntakeEnduser = await sdk.api.endusers.createOne({ email: 'multi-healthie-intake-hidden@tellescope.com' })
|
|
263
|
+
hiddenIntakeEnduserId = hiddenIntakeEnduser.id
|
|
264
|
+
|
|
265
|
+
await submitIntakeResponse(hiddenIntakeEnduserId, hiddenForm.id, {
|
|
266
|
+
fieldId: hiddenField.id, fieldTitle: hiddenField.title, answer: { type: 'Hidden Value', value: TAG },
|
|
267
|
+
})
|
|
268
|
+
await async_test(
|
|
269
|
+
"Hidden Value intake field sets healthieIntegrationId",
|
|
270
|
+
() => sdk.api.endusers.getOne(hiddenIntakeEnduserId),
|
|
271
|
+
{ onResult: e => e.healthieIntegrationId === TAG },
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// Multiple Choice answers arrive as arrays — the stored value must be the collapsed scalar,
|
|
275
|
+
// otherwise stringValidator100 rejects the update and takes the whole submission's intake down
|
|
276
|
+
const multipleChoiceForm = await sdk.api.forms.createOne({ title: "Multi Healthie Intake (Multiple Choice)" })
|
|
277
|
+
multipleChoiceFormId = multipleChoiceForm.id
|
|
278
|
+
const multipleChoiceField = await sdk.api.form_fields.createOne({
|
|
279
|
+
formId: multipleChoiceForm.id,
|
|
280
|
+
title: "Which clinic?",
|
|
281
|
+
type: 'multiple_choice',
|
|
282
|
+
intakeField: 'healthieIntegrationId',
|
|
283
|
+
options: { choices: [TAG, 'other-clinic'], radio: true },
|
|
284
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const multipleChoiceIntakeEnduser = await sdk.api.endusers.createOne({ email: 'multi-healthie-intake-mc@tellescope.com' })
|
|
288
|
+
multipleChoiceIntakeEnduserId = multipleChoiceIntakeEnduser.id
|
|
289
|
+
|
|
290
|
+
await submitIntakeResponse(multipleChoiceIntakeEnduserId, multipleChoiceForm.id, {
|
|
291
|
+
fieldId: multipleChoiceField.id, fieldTitle: multipleChoiceField.title, answer: { type: 'multiple_choice', value: [TAG] },
|
|
292
|
+
})
|
|
293
|
+
await async_test(
|
|
294
|
+
"Multiple Choice intake field stores healthieIntegrationId as a scalar",
|
|
295
|
+
() => sdk.api.endusers.getOne(multipleChoiceIntakeEnduserId),
|
|
296
|
+
{ onResult: e => e.healthieIntegrationId === TAG },
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
// Allowed form field types. The form_fields model validates intakeField as a free string (isFormField,
|
|
300
|
+
// which checks ENDUSER_FIELD_TYPES, is not wired into its schema), so this is enforced in the form editor
|
|
301
|
+
// rather than by the API — same as every other intake field. Assert the shared constant the editor reads.
|
|
302
|
+
assert(
|
|
303
|
+
objects_equivalent(
|
|
304
|
+
ENDUSER_FIELD_TYPES['healthieIntegrationId' as EnduserField],
|
|
305
|
+
['Hidden Value', 'Dropdown', 'multiple_choice', 'string'],
|
|
306
|
+
),
|
|
307
|
+
'ENDUSER_FIELD_TYPES error', 'healthieIntegrationId is limited to Hidden Value, Dropdown, Multiple Choice, and Text',
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// An incompatible type is accepted by the API but must never write a non-string to the enduser column
|
|
311
|
+
const numberField = await sdk.api.form_fields.createOne({
|
|
312
|
+
formId: multipleChoiceForm.id,
|
|
313
|
+
title: "Bad Type",
|
|
314
|
+
type: 'number',
|
|
315
|
+
intakeField: 'healthieIntegrationId',
|
|
316
|
+
previousFields: [{ type: 'after', info: { fieldId: multipleChoiceField.id } }],
|
|
317
|
+
})
|
|
318
|
+
const numberIntakeEnduser = await sdk.api.endusers.createOne({ email: 'multi-healthie-intake-number@tellescope.com' })
|
|
319
|
+
numberIntakeEnduserId = numberIntakeEnduser.id
|
|
320
|
+
await submitIntakeResponse(numberIntakeEnduserId, multipleChoiceForm.id, {
|
|
321
|
+
fieldId: numberField.id, fieldTitle: numberField.title, answer: { type: 'number', value: 5 },
|
|
322
|
+
})
|
|
323
|
+
await async_test(
|
|
324
|
+
"non-string answer for the healthieIntegrationId intake field is ignored",
|
|
325
|
+
() => sdk.api.endusers.getOne(numberIntakeEnduserId),
|
|
326
|
+
{ onResult: e => !e.healthieIntegrationId },
|
|
327
|
+
)
|
|
328
|
+
|
|
239
329
|
// ── Removing the primary must not affect the tagged integration; org list recomputes on delete ──
|
|
240
330
|
await sdk.api.integrations.deleteOne(primaryIntegrationId)
|
|
241
331
|
primaryIntegrationId = ''
|
|
@@ -258,9 +348,17 @@ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : {
|
|
|
258
348
|
if (!id) continue
|
|
259
349
|
await sdk.api.integrations.deleteOne(id).catch(console.error)
|
|
260
350
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
351
|
+
for (const id of formResponseIds) {
|
|
352
|
+
await sdk.api.form_responses.deleteOne(id).catch(console.error)
|
|
353
|
+
}
|
|
354
|
+
for (const id of [enduserId, formsortEnduserId, hiddenIntakeEnduserId, multipleChoiceIntakeEnduserId, numberIntakeEnduserId]) {
|
|
355
|
+
if (!id) continue
|
|
356
|
+
await sdk.api.endusers.deleteOne(id).catch(console.error)
|
|
357
|
+
}
|
|
358
|
+
for (const id of [formId, hiddenFormId, multipleChoiceFormId]) {
|
|
359
|
+
if (!id) continue
|
|
360
|
+
await sdk.api.forms.deleteOne(id).catch(console.error)
|
|
361
|
+
}
|
|
264
362
|
}
|
|
265
363
|
}
|
|
266
364
|
|