@antelopejs/dms 0.15.0 → 0.16.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/CHANGELOG.md +47 -0
- package/README.md +36 -26
- package/bin/control/base.mjs +9 -4
- package/bin/control/browser-client.mjs +24 -5
- package/bin/control/browser-wire.mjs +41 -0
- package/bin/control/browser-wire.test.mjs +96 -0
- package/bin/control/browser.mjs +78 -27
- package/bin/control/commands-stack.mjs +58 -8
- package/bin/control/commands-verify.mjs +47 -1
- package/bin/control/doctor.mjs +49 -4
- package/bin/control/failure.mjs +9 -0
- package/bin/control/journey.mjs +163 -30
- package/bin/control/journey.test.mjs +61 -0
- package/bin/control/json.mjs +229 -0
- package/bin/control/json.test.mjs +130 -0
- package/bin/control/mail.mjs +57 -7
- package/bin/control/mail.test.mjs +55 -0
- package/bin/control/mongo.mjs +205 -27
- package/bin/control/mongo.test.mjs +298 -0
- package/bin/control/options.mjs +33 -0
- package/bin/control/processes.mjs +24 -2
- package/bin/control/renderer.mjs +39 -0
- package/bin/control/renderer.test.mjs +324 -0
- package/bin/control/run-state.mjs +119 -6
- package/bin/control/run-state.test.mjs +171 -0
- package/bin/control/session.mjs +31 -14
- package/bin/control/stack.mjs +96 -24
- package/bin/control/tab.mjs +117 -18
- package/bin/control/verify/behaviour.mjs +44 -6
- package/bin/control/verify/behaviour.test.mjs +130 -22
- package/bin/control/verify/decode.mjs +220 -0
- package/bin/control/verify/decode.test.mjs +245 -0
- package/bin/control/verify/doors.mjs +1 -1
- package/bin/control/verify/doors.test.mjs +40 -0
- package/bin/control/verify/emit.mjs +135 -9
- package/bin/control/verify/formal.mjs +61 -10
- package/bin/control/verify/formal.test.mjs +19 -3
- package/bin/control/verify/judgement.mjs +22 -3
- package/bin/control/verify/map.mjs +25 -5
- package/bin/control/verify/publish.mjs +20 -14
- package/bin/control/verify/seeded.mjs +65 -16
- package/bin/control/verify/skill.mjs +27 -6
- package/bin/control/verify/types.mjs +29 -0
- package/bin/control/verify/verdict.mjs +72 -7
- package/bin/dms-control.mjs +8 -28
- package/checks/api-error-presentation.d.mts +61 -0
- package/dist/attachments/access.js +1 -1
- package/dist/attachments/access.js.map +1 -1
- package/dist/attachments/fields.d.ts +2 -2
- package/dist/attachments/fields.js +26 -13
- package/dist/attachments/fields.js.map +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/index.js.map +1 -1
- package/dist/attachments/read.js.map +1 -1
- package/dist/attachments/save.d.ts +1 -1
- package/dist/attachments/save.js +49 -17
- package/dist/attachments/save.js.map +1 -1
- package/dist/attachments/table-view.d.ts +1 -2
- package/dist/attachments/table-view.js +23 -13
- package/dist/attachments/table-view.js.map +1 -1
- package/dist/automation/events.d.ts +10 -10
- package/dist/automation/index.js +3 -4
- package/dist/automation/index.js.map +1 -1
- package/dist/automation/send-notification.d.ts +2 -2
- package/dist/automation/send-notification.js +4 -6
- package/dist/automation/send-notification.js.map +1 -1
- package/dist/config/internal/state.d.ts +7 -0
- package/dist/config/internal/state.js +9 -0
- package/dist/config/internal/state.js.map +1 -1
- package/dist/config/startup-config.d.ts +1 -0
- package/dist/config/startup-config.js +11 -0
- package/dist/config/startup-config.js.map +1 -1
- package/dist/crons/cleanup-user-invites.js.map +1 -1
- package/dist/crons/sweep-expired-sessions.js.map +1 -1
- package/dist/crons/sweep-stale-exports.js.map +1 -1
- package/dist/db/guard-row.d.ts +2 -3
- package/dist/db/guard-row.js +1 -2
- package/dist/db/guard-row.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/models/attachments.model.d.ts +7 -5
- package/dist/db/models/index.d.ts +1 -0
- package/dist/db/models/index.js +1 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/setupToken.model.d.ts +31 -0
- package/dist/db/models/setupToken.model.js +62 -0
- package/dist/db/models/setupToken.model.js.map +1 -0
- package/dist/db/models/signInAttempts.model.d.ts +7 -5
- package/dist/db/models/systemState.model.d.ts +7 -5
- package/dist/db/models/userEmailChanges.model.d.ts +7 -5
- package/dist/db/models/userKnownDevices.model.d.ts +7 -5
- package/dist/db/models/userNotificationPreferences.model.d.ts +7 -5
- package/dist/db/models/userNotifications.model.d.ts +9 -6
- package/dist/db/models/userNotifications.model.js.map +1 -1
- package/dist/db/setup-token.d.ts +2 -0
- package/dist/db/setup-token.js +28 -0
- package/dist/db/setup-token.js.map +1 -0
- package/dist/db/tables/index.d.ts +1 -0
- package/dist/db/tables/index.js +1 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/setupToken.table.d.ts +7 -0
- package/dist/db/tables/setupToken.table.js +34 -0
- package/dist/db/tables/setupToken.table.js.map +1 -0
- package/dist/dev/module-update-notifications.js.map +1 -1
- package/dist/hooks/internal/sign-in-monitor.js +6 -6
- package/dist/hooks/internal/sign-in-monitor.js.map +1 -1
- package/dist/implementations/dms/page/controller.d.ts +2 -2
- package/dist/implementations/dms/page/controller.js +3 -1
- package/dist/implementations/dms/page/controller.js.map +1 -1
- package/dist/implementations/dms/page/dynamic-menu.js +3 -2
- package/dist/implementations/dms/page/dynamic-menu.js.map +1 -1
- package/dist/implementations/dms/page/notifications.d.ts +6 -2
- package/dist/implementations/dms/page/notifications.js +1 -0
- package/dist/implementations/dms/page/notifications.js.map +1 -1
- package/dist/implementations/dms/page/payloads.js +6 -10
- package/dist/implementations/dms/page/payloads.js.map +1 -1
- package/dist/implementations/dms/page/preview-pages.js +6 -3
- package/dist/implementations/dms/page/preview-pages.js.map +1 -1
- package/dist/implementations/dms/page/public-layout.d.ts +2 -0
- package/dist/implementations/dms/page/public-layout.js +45 -0
- package/dist/implementations/dms/page/public-layout.js.map +1 -0
- package/dist/implementations/dms/page/registry.js +10 -4
- package/dist/implementations/dms/page/registry.js.map +1 -1
- package/dist/implementations/dms/permissions.js +12 -12
- package/dist/implementations/dms/permissions.js.map +1 -1
- package/dist/implementations/dms-base/bulk-selection.d.ts +3 -4
- package/dist/implementations/dms-base/bulk-selection.js +18 -6
- package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
- package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
- package/dist/implementations/dms-base/footer-summary.js +44 -27
- package/dist/implementations/dms-base/footer-summary.js.map +1 -1
- package/dist/implementations/dms-base/row-rule-operand.d.ts +6 -0
- package/dist/implementations/dms-base/row-rule-operand.js +41 -0
- package/dist/implementations/dms-base/row-rule-operand.js.map +1 -0
- package/dist/implementations/dms-base/search-route.d.ts +9 -13
- package/dist/implementations/dms-base/search-route.js +7 -6
- package/dist/implementations/dms-base/search-route.js.map +1 -1
- package/dist/implementations/dms-base/services.d.ts +1 -0
- package/dist/implementations/dms-base/services.js +1 -0
- package/dist/implementations/dms-base/services.js.map +1 -1
- package/dist/implementations/dms-base/table-view.d.ts +5 -6
- package/dist/implementations/dms-base/table-view.js +17 -6
- package/dist/implementations/dms-base/table-view.js.map +1 -1
- package/dist/implementations/dms-html-render/index.js +16 -6
- package/dist/implementations/dms-html-render/index.js.map +1 -1
- package/dist/implementations/dms-notifications/delivery-guard.d.ts +3 -2
- package/dist/implementations/dms-notifications/delivery-guard.js.map +1 -1
- package/dist/implementations/dms-notifications/index.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/pages/public/preview/block-types.d.ts +1 -2
- package/dist/pages/public/preview/block-types.js.map +1 -1
- package/dist/pages/settings/overview/account-summary.js +4 -1
- package/dist/pages/settings/overview/account-summary.js.map +1 -1
- package/dist/pages/settings/users/account-activity-feed.js +3 -2
- package/dist/pages/settings/users/account-activity-feed.js.map +1 -1
- package/dist/pages/settings/users/account-credentials.d.ts +1 -1
- package/dist/pages/settings/users/account-credentials.js +1 -2
- package/dist/pages/settings/users/account-credentials.js.map +1 -1
- package/dist/pages/settings/users/account-data-store.d.ts +1 -1
- package/dist/pages/settings/users/account-data-store.js +1 -3
- package/dist/pages/settings/users/account-data-store.js.map +1 -1
- package/dist/pages/settings/users/current-password.js +1 -1
- package/dist/pages/settings/users/current-password.js.map +1 -1
- package/dist/pages/settings/users/email-change.d.ts +2 -2
- package/dist/pages/settings/users/email-change.js +2 -3
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invite-extension-routes.d.ts +2 -2
- package/dist/pages/settings/users/invite-extension-routes.js +11 -4
- package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
- package/dist/pages/settings/users/invites.d.ts +2 -2
- package/dist/pages/settings/users/member-invite-batch.d.ts +2 -2
- package/dist/pages/settings/users/member-invite-batch.js +7 -3
- package/dist/pages/settings/users/member-invite-batch.js.map +1 -1
- package/dist/pages/settings/users/member-roles-field.js +1 -4
- package/dist/pages/settings/users/member-roles-field.js.map +1 -1
- package/dist/pages/settings/users/members.d.ts +2 -3
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.d.ts +2 -3
- package/dist/pages/settings/users/notifications.js +2 -1
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/pages/settings/users/profile-helpers.d.ts +0 -1
- package/dist/pages/settings/users/profile-helpers.js +0 -17
- package/dist/pages/settings/users/profile-helpers.js.map +1 -1
- package/dist/pages/settings/users/profile.d.ts +3 -4
- package/dist/pages/settings/users/profile.js +12 -12
- package/dist/pages/settings/users/profile.js.map +1 -1
- package/dist/pages/settings/users/region.d.ts +3 -4
- package/dist/pages/settings/users/region.js.map +1 -1
- package/dist/pages/settings/users/regional-preferences.d.ts +8 -1
- package/dist/pages/settings/users/regional-preferences.js +10 -6
- package/dist/pages/settings/users/regional-preferences.js.map +1 -1
- package/dist/pages/settings/users/role-preview.js +3 -2
- package/dist/pages/settings/users/role-preview.js.map +1 -1
- package/dist/pages/settings/users/roles.d.ts +4 -5
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/pages/settings/users/security.d.ts +9 -12
- package/dist/pages/settings/users/security.js +11 -9
- package/dist/pages/settings/users/security.js.map +1 -1
- package/dist/pages/settings/users/two-factor-operations.d.ts +7 -7
- package/dist/pages/settings/users/two-factor-operations.js +5 -7
- package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
- package/dist/realtime/broker.js.map +1 -1
- package/dist/realtime/heartbeat.js.map +1 -1
- package/dist/realtime/redis-broker.js.map +1 -1
- package/dist/realtime/sessions.js.map +1 -1
- package/dist/realtime/sse.js +1 -2
- package/dist/realtime/sse.js.map +1 -1
- package/dist/routes/auth/forgot-password.d.ts +1 -1
- package/dist/routes/auth/forgot-password.js +1 -3
- package/dist/routes/auth/forgot-password.js.map +1 -1
- package/dist/routes/auth/login.d.ts +2 -2
- package/dist/routes/auth/login.js +8 -4
- package/dist/routes/auth/login.js.map +1 -1
- package/dist/routes/auth/logout.d.ts +1 -1
- package/dist/routes/auth/logout.js +1 -2
- package/dist/routes/auth/logout.js.map +1 -1
- package/dist/routes/auth/oauth/providers.js +30 -6
- package/dist/routes/auth/oauth/providers.js.map +1 -1
- package/dist/routes/auth/oauth/state.js +8 -5
- package/dist/routes/auth/oauth/state.js.map +1 -1
- package/dist/routes/auth/oauth/token-exchange.js +8 -1
- package/dist/routes/auth/oauth/token-exchange.js.map +1 -1
- package/dist/routes/auth/password-hash-upgrade.d.ts +2 -0
- package/dist/routes/auth/password-hash-upgrade.js +22 -0
- package/dist/routes/auth/password-hash-upgrade.js.map +1 -0
- package/dist/routes/auth/refresh.d.ts +1 -1
- package/dist/routes/auth/refresh.js +1 -2
- package/dist/routes/auth/refresh.js.map +1 -1
- package/dist/routes/auth/request-2fa-email.d.ts +1 -1
- package/dist/routes/auth/request-2fa-email.js +1 -2
- package/dist/routes/auth/request-2fa-email.js.map +1 -1
- package/dist/routes/auth/reset-password.d.ts +1 -1
- package/dist/routes/auth/reset-password.js +1 -3
- package/dist/routes/auth/reset-password.js.map +1 -1
- package/dist/routes/auth/session-handoff.d.ts +1 -1
- package/dist/routes/auth/session-handoff.js +1 -2
- package/dist/routes/auth/session-handoff.js.map +1 -1
- package/dist/routes/auth/signup.d.ts +1 -1
- package/dist/routes/auth/signup.js +1 -2
- package/dist/routes/auth/signup.js.map +1 -1
- package/dist/routes/auth/switch-tenant.d.ts +2 -3
- package/dist/routes/auth/switch-tenant.js +3 -2
- package/dist/routes/auth/switch-tenant.js.map +1 -1
- package/dist/routes/auth/validate-forgot-password-token.d.ts +1 -1
- package/dist/routes/auth/validate-forgot-password-token.js +1 -3
- package/dist/routes/auth/validate-forgot-password-token.js.map +1 -1
- package/dist/routes/auth/validate-invite-token.d.ts +1 -1
- package/dist/routes/auth/validate-invite-token.js +1 -3
- package/dist/routes/auth/validate-invite-token.js.map +1 -1
- package/dist/routes/auth/verify-2fa.d.ts +1 -1
- package/dist/routes/auth/verify-2fa.js +1 -2
- package/dist/routes/auth/verify-2fa.js.map +1 -1
- package/dist/routes/auth/verify-email.d.ts +1 -1
- package/dist/routes/auth/verify-email.js +1 -2
- package/dist/routes/auth/verify-email.js.map +1 -1
- package/dist/routes/auth.d.ts +14 -15
- package/dist/routes/auth.js +15 -13
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/authentication.d.ts +2 -0
- package/dist/routes/authentication.js +2 -0
- package/dist/routes/authentication.js.map +1 -1
- package/dist/routes/files.d.ts +1 -2
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/onboarding.d.ts +4 -3
- package/dist/routes/onboarding.js +20 -3
- package/dist/routes/onboarding.js.map +1 -1
- package/dist/routes/realtime.js +5 -6
- package/dist/routes/realtime.js.map +1 -1
- package/dist/routes/store.d.ts +6 -7
- package/dist/routes/store.js +19 -13
- package/dist/routes/store.js.map +1 -1
- package/dist/store/catalog.d.ts +13 -5
- package/dist/store/catalog.js +19 -9
- package/dist/store/catalog.js.map +1 -1
- package/dist/store/compatibility.d.ts +1 -0
- package/dist/store/compatibility.js +24 -8
- package/dist/store/compatibility.js.map +1 -1
- package/dist/store/frontend-compatibility.js +15 -3
- package/dist/store/frontend-compatibility.js.map +1 -1
- package/dist/store/interfaces.js +35 -13
- package/dist/store/interfaces.js.map +1 -1
- package/dist/store/internal/frontend/frontend-modules.js +7 -3
- package/dist/store/internal/frontend/frontend-modules.js.map +1 -1
- package/dist/store/internal/frontend/frontend-ready.d.ts +2 -1
- package/dist/store/internal/frontend/frontend-ready.js +6 -4
- package/dist/store/internal/frontend/frontend-ready.js.map +1 -1
- package/dist/store/internal/frontend/manifest.js +6 -7
- package/dist/store/internal/frontend/manifest.js.map +1 -1
- package/dist/store/operations.js.map +1 -1
- package/dist/store/registry.js.map +1 -1
- package/dist/store/runtime.d.ts +13 -0
- package/dist/store/runtime.js +9 -1
- package/dist/store/runtime.js.map +1 -1
- package/dist/utils/admin-invite-email.js +3 -1
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-subjects.js +3 -1
- package/dist/utils/email-subjects.js.map +1 -1
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +126 -8
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/utils/export-jobs-run.js.map +1 -1
- package/dist/utils/export-jobs.js +3 -9
- package/dist/utils/export-jobs.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/own-value.d.ts +1 -0
- package/dist/utils/own-value.js +7 -0
- package/dist/utils/own-value.js.map +1 -0
- package/dist/utils/row-action-rule-evaluator.js +31 -35
- package/dist/utils/row-action-rule-evaluator.js.map +1 -1
- package/dist/utils/sign-in-country.js +4 -2
- package/dist/utils/sign-in-country.js.map +1 -1
- package/dist/utils/tenant-export-archive.js +9 -14
- package/dist/utils/tenant-export-archive.js.map +1 -1
- package/dist/utils/user-agent.js +2 -1
- package/dist/utils/user-agent.js.map +1 -1
- package/dist/validation/auth.schema.d.ts +10 -10
- package/dist/validation/onboarding/register-admin.schema.d.ts +5 -0
- package/dist/validation/onboarding/register-admin.schema.js +3 -0
- package/dist/validation/onboarding/register-admin.schema.js.map +1 -1
- package/dist/validation/regional-preferences.schema.d.ts +2 -2
- package/dist/validation/role-editor.schema.d.ts +1 -1
- package/frontend-vue/build/backend-boundary.ts +49 -18
- package/frontend-vue/dms.frontend.ts +8 -12
- package/frontend-vue/layers/dms-auth/server/accounts.ts +44 -3
- package/frontend-vue/layers/dms-auth/server/actions.ts +17 -4
- package/frontend-vue/layers/dms-auth/server/oauth.ts +42 -4
- package/frontend-vue/layers/dms-core/app/types/watch.ts +1 -2
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +1 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +1 -0
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +9 -9
- package/frontend-vue/layers/dms-layout/app/components/layout/AppLogo.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useInstalledModules.ts +78 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +5 -24
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +2 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -1
- package/frontend-vue/layers/dms-onboarding/app/components/onboarding/steps/register.vue +21 -1
- package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +14 -0
- package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-en-GB.json +2 -0
- package/frontend-vue/layers/dms-onboarding/i18n/locales/onboarding-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +57 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +7 -9
- package/frontend-vue/layers/dms-ui/app/entries/composables/form/registerFormFunctions.ts +1 -0
- package/frontend-vue/lazy-plugin.ts +18 -0
- package/frontend-vue/package.json +2 -2
- package/package.json +19 -13
- package/skills/dms-project/SKILL.md +4 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
arrayOf,
|
|
8
|
+
decode,
|
|
9
|
+
firstString,
|
|
10
|
+
instanceOf,
|
|
11
|
+
isNumber,
|
|
12
|
+
isRecord,
|
|
13
|
+
isString,
|
|
14
|
+
nullable,
|
|
15
|
+
objectOf,
|
|
16
|
+
oneOf,
|
|
17
|
+
readRecordList,
|
|
18
|
+
recordOf,
|
|
19
|
+
stringField,
|
|
20
|
+
} from "./json.mjs";
|
|
21
|
+
|
|
22
|
+
await test("an object is a record, an array, null and a string are not", () => {
|
|
23
|
+
assert.equal(isRecord({}), true);
|
|
24
|
+
assert.equal(isRecord([]), false);
|
|
25
|
+
assert.equal(isRecord(null), false);
|
|
26
|
+
assert.equal(isRecord("text"), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await test("a string field answers only an own string property", () => {
|
|
30
|
+
assert.equal(stringField({ access_token: "abc" }, "access_token"), "abc");
|
|
31
|
+
assert.equal(stringField({ access_token: 4 }, "access_token"), undefined);
|
|
32
|
+
assert.equal(stringField({}, "access_token"), undefined);
|
|
33
|
+
assert.equal(stringField(["abc"], "0"), undefined);
|
|
34
|
+
assert.equal(stringField(null, "access_token"), undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await test("a string field never answers an inherited property", () => {
|
|
38
|
+
assert.equal(stringField({}, "constructor"), undefined);
|
|
39
|
+
assert.equal(stringField({}, "toString"), undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await test("the first string of a list is its first element only", () => {
|
|
43
|
+
assert.equal(firstString(["user-1", "user-2"]), "user-1");
|
|
44
|
+
assert.equal(firstString([4, "user-2"]), undefined);
|
|
45
|
+
assert.equal(firstString([]), undefined);
|
|
46
|
+
assert.equal(firstString({ 0: "user-1" }), undefined);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import("node:test").TestContext} t The test
|
|
51
|
+
* @param {string} text What the file holds
|
|
52
|
+
* @returns {string} The file
|
|
53
|
+
*/
|
|
54
|
+
function jsonFile(t, text) {
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), "dms-control-json-test-"));
|
|
56
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
57
|
+
const file = join(dir, "list.json");
|
|
58
|
+
writeFileSync(file, text);
|
|
59
|
+
return file;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await test("a list of objects is read as written", (t) => {
|
|
63
|
+
const file = jsonFile(t, '[{"journey":"smoke","extra":1},{}]');
|
|
64
|
+
assert.deepEqual(readRecordList(file), [{ journey: "smoke", extra: 1 }, {}]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await test("a file that is not a list of objects is refused", (t) => {
|
|
68
|
+
for (const text of ["{}", "null", "[1]", "[null]", "[[]]"]) {
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => readRecordList(jsonFile(t, text)),
|
|
71
|
+
/is not a list of objects/,
|
|
72
|
+
text,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await test("a file that is not JSON is refused", (t) => {
|
|
78
|
+
assert.throws(() => readRecordList(jsonFile(t, "[")), SyntaxError);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test("an object guard checks required and optional properties and keeps the rest", () => {
|
|
82
|
+
const guard = objectOf({ name: isString }, { count: isNumber });
|
|
83
|
+
assert.equal(guard({ name: "a" }), true);
|
|
84
|
+
assert.equal(guard({ name: "a", count: 2, extra: true }), true);
|
|
85
|
+
assert.equal(guard({ name: "a", count: "2" }), false);
|
|
86
|
+
assert.equal(guard({ count: 2 }), false);
|
|
87
|
+
assert.equal(guard([]), false);
|
|
88
|
+
assert.equal(guard(null), false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test("an object guard never counts an inherited property", () => {
|
|
92
|
+
assert.equal(objectOf({ constructor: isString })({}), false);
|
|
93
|
+
assert.equal(objectOf({}, { toString: isString })({}), true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await test("list, record, nullable and name guards check what they hold", () => {
|
|
97
|
+
assert.equal(arrayOf(isString)(["a", 1]), false);
|
|
98
|
+
assert.equal(arrayOf(isString)([]), true);
|
|
99
|
+
assert.equal(recordOf(isNumber)({ a: 1, b: "x" }), false);
|
|
100
|
+
assert.equal(nullable(isString)(null), true);
|
|
101
|
+
assert.equal(nullable(isString)(3), false);
|
|
102
|
+
assert.equal(oneOf(["PASS", "FAIL"])("PASSED"), false);
|
|
103
|
+
assert.equal(instanceOf(RegExp)(/x/), true);
|
|
104
|
+
assert.equal(instanceOf(RegExp)("x"), false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test("decode returns a value that passes and names the source of one that does not", () => {
|
|
108
|
+
const value = { name: "a" };
|
|
109
|
+
assert.equal(
|
|
110
|
+
decode(objectOf({ name: isString }), value, "x.json", "a thing"),
|
|
111
|
+
value,
|
|
112
|
+
);
|
|
113
|
+
assert.throws(
|
|
114
|
+
() => decode(objectOf({ name: isString }), {}, "x.json", "a thing"),
|
|
115
|
+
/x\.json is malformed: expected a thing/,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test("a record guard refuses what is no object, a list included", () => {
|
|
120
|
+
assert.equal(recordOf(isString)("abc"), false);
|
|
121
|
+
assert.equal(recordOf(isNumber)([1, 2]), false);
|
|
122
|
+
assert.equal(recordOf(isNumber)(null), false);
|
|
123
|
+
assert.equal(recordOf(isNumber)({}), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await test("a nullable guard accepts null and refuses a missing value", () => {
|
|
127
|
+
assert.equal(nullable(isString)(undefined), false);
|
|
128
|
+
assert.equal(nullable(isString)(null), true);
|
|
129
|
+
assert.equal(objectOf({ owner: nullable(isString) })({}), false);
|
|
130
|
+
});
|
package/bin/control/mail.mjs
CHANGED
|
@@ -7,24 +7,52 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { createServer } from "node:net";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { arrayOf, decode, isString, objectOf } from "./json.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ from: string, to: string[] }} Envelope
|
|
14
|
+
* @typedef {{ buffer: string, inData: boolean, envelope: Envelope }} SmtpSession
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const isSummary = objectOf({
|
|
18
|
+
from: isString,
|
|
19
|
+
to: arrayOf(isString),
|
|
20
|
+
subject: isString,
|
|
21
|
+
links: arrayOf(isString),
|
|
22
|
+
text: isString,
|
|
23
|
+
receivedAt: isString,
|
|
24
|
+
});
|
|
10
25
|
|
|
11
26
|
const END_OF_DATA = "\r\n.\r\n";
|
|
12
27
|
const QUOTED_SOFT_BREAK = /=\r?\n/g;
|
|
13
28
|
const QUOTED_BYTE = /=([0-9A-F]{2})/g;
|
|
14
29
|
const HEX = 16;
|
|
15
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {import("node:net").Socket} socket
|
|
33
|
+
* @param {string} line
|
|
34
|
+
*/
|
|
16
35
|
function reply(socket, line) {
|
|
17
36
|
socket.write(`${line}\r\n`);
|
|
18
37
|
}
|
|
19
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
20
43
|
function decodeQuotedPrintable(text) {
|
|
21
44
|
return text
|
|
22
45
|
.replace(QUOTED_SOFT_BREAK, "")
|
|
23
|
-
.replace(QUOTED_BYTE, (_match, byte) =>
|
|
46
|
+
.replace(QUOTED_BYTE, (_match, /** @type {string} */ byte) =>
|
|
24
47
|
String.fromCharCode(Number.parseInt(byte, HEX)),
|
|
25
48
|
);
|
|
26
49
|
}
|
|
27
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} message
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
28
56
|
function headerOf(message, name) {
|
|
29
57
|
const match = new RegExp(`^${name}:\\s*(.+(?:\\r?\\n[ \\t].+)*)`, "im").exec(
|
|
30
58
|
message,
|
|
@@ -32,6 +60,7 @@ function headerOf(message, name) {
|
|
|
32
60
|
return match ? match[1].replace(/\r?\n[ \t]+/g, " ").trim() : "";
|
|
33
61
|
}
|
|
34
62
|
|
|
63
|
+
/** @type {Record<string, string>} */
|
|
35
64
|
const ENTITIES = {
|
|
36
65
|
amp: "&",
|
|
37
66
|
lt: "<",
|
|
@@ -41,6 +70,10 @@ const ENTITIES = {
|
|
|
41
70
|
nbsp: " ",
|
|
42
71
|
};
|
|
43
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} html
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
44
77
|
function readableText(html) {
|
|
45
78
|
const body = html.slice(html.search(/\r?\n\r?\n/));
|
|
46
79
|
return body
|
|
@@ -51,6 +84,11 @@ function readableText(html) {
|
|
|
51
84
|
.trim();
|
|
52
85
|
}
|
|
53
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} folder
|
|
89
|
+
* @param {Envelope} envelope
|
|
90
|
+
* @param {string} raw
|
|
91
|
+
*/
|
|
54
92
|
function saveMessage(folder, envelope, raw) {
|
|
55
93
|
mkdirSync(folder, { recursive: true });
|
|
56
94
|
const index =
|
|
@@ -76,6 +114,11 @@ function saveMessage(folder, envelope, raw) {
|
|
|
76
114
|
);
|
|
77
115
|
}
|
|
78
116
|
|
|
117
|
+
/**
|
|
118
|
+
* @param {SmtpSession} state
|
|
119
|
+
* @param {import("node:net").Socket} socket
|
|
120
|
+
* @param {string} line
|
|
121
|
+
*/
|
|
79
122
|
function command(state, socket, line) {
|
|
80
123
|
const verb = line.slice(0, 4).toUpperCase();
|
|
81
124
|
if (verb === "EHLO" || verb === "HELO")
|
|
@@ -99,11 +142,13 @@ function command(state, socket, line) {
|
|
|
99
142
|
return reply(socket, "250 OK");
|
|
100
143
|
}
|
|
101
144
|
|
|
145
|
+
/** @param {string} folder */
|
|
102
146
|
function session(folder) {
|
|
103
|
-
return (socket) => {
|
|
147
|
+
return (/** @type {import("node:net").Socket} */ socket) => {
|
|
148
|
+
/** @type {SmtpSession} */
|
|
104
149
|
const state = { buffer: "", inData: false, envelope: { from: "", to: [] } };
|
|
105
150
|
reply(socket, "220 dms-control mail sink");
|
|
106
|
-
socket.on("data", (chunk) => {
|
|
151
|
+
socket.on("data", (/** @type {Buffer} */ chunk) => {
|
|
107
152
|
state.buffer += chunk.toString("latin1");
|
|
108
153
|
while (state.buffer.length > 0) {
|
|
109
154
|
if (state.inData) {
|
|
@@ -156,8 +201,13 @@ export function capturedMail(folder) {
|
|
|
156
201
|
return readdirSync(folder)
|
|
157
202
|
.filter((name) => name.endsWith(".json"))
|
|
158
203
|
.sort()
|
|
159
|
-
.map((name) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
.map((name) => {
|
|
205
|
+
const summary = join(folder, name);
|
|
206
|
+
/** @type {unknown} */
|
|
207
|
+
const stored = JSON.parse(readFileSync(summary, "utf8"));
|
|
208
|
+
return {
|
|
209
|
+
file: join(folder, name.replace(/\.json$/, ".eml")),
|
|
210
|
+
...decode(isSummary, stored, summary, "a captured message"),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
163
213
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { capturedMail } from "./mail.mjs";
|
|
7
|
+
|
|
8
|
+
const SUMMARY = {
|
|
9
|
+
from: "no-reply@example.com",
|
|
10
|
+
to: ["a@example.com"],
|
|
11
|
+
subject: "Welcome",
|
|
12
|
+
links: ["https://example.com/verify"],
|
|
13
|
+
text: "Hello",
|
|
14
|
+
receivedAt: "2026-11-02T00:00:00.000Z",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import("node:test").TestContext} t The test
|
|
19
|
+
* @param {unknown} summary What the sink stored for its one message
|
|
20
|
+
* @returns {string} The sink's folder
|
|
21
|
+
*/
|
|
22
|
+
function sink(t, summary) {
|
|
23
|
+
const folder = mkdtempSync(join(tmpdir(), "dms-control-mail-test-"));
|
|
24
|
+
t.after(() => rmSync(folder, { recursive: true, force: true }));
|
|
25
|
+
writeFileSync(join(folder, "001.json"), JSON.stringify(summary));
|
|
26
|
+
return folder;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await test("a captured message reads back with the path of its raw file", (t) => {
|
|
30
|
+
const folder = sink(t, SUMMARY);
|
|
31
|
+
assert.deepEqual(capturedMail(folder), [
|
|
32
|
+
{ file: join(folder, "001.eml"), ...SUMMARY },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await test("a stored summary whose recipients are no list is refused", (t) => {
|
|
37
|
+
const folder = sink(t, { ...SUMMARY, to: "a@example.com" });
|
|
38
|
+
assert.throws(
|
|
39
|
+
() => capturedMail(folder),
|
|
40
|
+
/001\.json is malformed: expected a captured message/,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await test("a folder that does not exist holds no mail", () => {
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
capturedMail(join(tmpdir(), "dms-control-no-such-sink")),
|
|
47
|
+
[],
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await test("a stored summary without its links is refused", (t) => {
|
|
52
|
+
const { links: _links, ...withoutLinks } = SUMMARY;
|
|
53
|
+
const folder = sink(t, withoutLinks);
|
|
54
|
+
assert.throws(() => capturedMail(folder), /001\.json is malformed/);
|
|
55
|
+
});
|
package/bin/control/mongo.mjs
CHANGED
|
@@ -3,14 +3,117 @@ import { mkdirSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { arrayOf, decode, isRecord, isString, objectOf } from "./json.mjs";
|
|
6
7
|
import { freePort, startDetached, waitForPort } from "./processes.mjs";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {(...args: unknown[]) => unknown} Callable A function whose result is read as `unknown`
|
|
11
|
+
* @typedef {new (...args: unknown[]) => unknown} Constructor A class whose instances are read as `unknown`
|
|
12
|
+
* @typedef {{ connect: Callable, close: Callable, db: Callable }} Client The calls made on a driver client
|
|
13
|
+
* @typedef {{ admin: Callable, dropDatabase: Callable }} Database The calls made on a driver database
|
|
14
|
+
* @typedef {{ listDatabases: Callable }} Admin The call made on a driver's admin handle
|
|
15
|
+
*/
|
|
16
|
+
|
|
8
17
|
/** The server version the backend test harness pins, already cached by it. */
|
|
9
18
|
const MONGO_VERSION = "8.0.8";
|
|
10
19
|
const READY_TIMEOUT_MS = 30_000;
|
|
11
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @param {unknown} value
|
|
23
|
+
* @returns {value is object} True for an object or a function, which can hold members
|
|
24
|
+
*/
|
|
25
|
+
function isObjectLike(value) {
|
|
26
|
+
return (
|
|
27
|
+
(typeof value === "object" && value !== null) || typeof value === "function"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {unknown} value
|
|
33
|
+
* @returns {value is Callable} True for a function
|
|
34
|
+
*/
|
|
35
|
+
function isCallable(value) {
|
|
36
|
+
return typeof value === "function";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {unknown} value
|
|
41
|
+
* @returns {value is Constructor} True for a function that `new` accepts, such as a class or a bound class, and false for an arrow function or a method
|
|
42
|
+
*/
|
|
43
|
+
function isConstructor(value) {
|
|
44
|
+
if (!isCallable(value)) return false;
|
|
45
|
+
try {
|
|
46
|
+
Reflect.construct(Object, [], value);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {unknown} value
|
|
55
|
+
* @returns {value is { getPath: Callable }} True for an object with a `getPath` function
|
|
56
|
+
*/
|
|
57
|
+
function isBinaryResolver(value) {
|
|
58
|
+
return isObjectLike(value) && "getPath" in value && isCallable(value.getPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @returns {value is Client} True for an object with `connect`, `close` and `db` functions
|
|
64
|
+
*/
|
|
65
|
+
function isClient(value) {
|
|
66
|
+
return (
|
|
67
|
+
isObjectLike(value) &&
|
|
68
|
+
"connect" in value &&
|
|
69
|
+
isCallable(value.connect) &&
|
|
70
|
+
"close" in value &&
|
|
71
|
+
isCallable(value.close) &&
|
|
72
|
+
"db" in value &&
|
|
73
|
+
isCallable(value.db)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} value
|
|
79
|
+
* @returns {value is Database} True for an object with `admin` and `dropDatabase` functions
|
|
80
|
+
*/
|
|
81
|
+
function isDatabase(value) {
|
|
82
|
+
return (
|
|
83
|
+
isObjectLike(value) &&
|
|
84
|
+
"admin" in value &&
|
|
85
|
+
isCallable(value.admin) &&
|
|
86
|
+
"dropDatabase" in value &&
|
|
87
|
+
isCallable(value.dropDatabase)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {unknown} value
|
|
93
|
+
* @returns {value is Admin} True for an object with a `listDatabases` function
|
|
94
|
+
*/
|
|
95
|
+
function isAdmin(value) {
|
|
96
|
+
return (
|
|
97
|
+
isObjectLike(value) &&
|
|
98
|
+
"listDatabases" in value &&
|
|
99
|
+
isCallable(value.listDatabases)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isDatabaseList = objectOf({
|
|
104
|
+
databases: arrayOf(objectOf({ name: isString })),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {unknown} namespace A module namespace
|
|
109
|
+
* @returns {Record<string, unknown>} Its exports, with those of a CommonJS `default` added
|
|
110
|
+
*/
|
|
12
111
|
function withDefaultExports(namespace) {
|
|
13
|
-
|
|
112
|
+
if (!isRecord(namespace))
|
|
113
|
+
throw new TypeError("A module namespace is not an object");
|
|
114
|
+
const fallback = namespace.default;
|
|
115
|
+
const exports = isObjectLike(fallback) ? fallback : {};
|
|
116
|
+
return { ...exports, ...namespace };
|
|
14
117
|
}
|
|
15
118
|
|
|
16
119
|
/**
|
|
@@ -19,16 +122,18 @@ function withDefaultExports(namespace) {
|
|
|
19
122
|
*
|
|
20
123
|
* @param {string} name The package name
|
|
21
124
|
* @param {string[]} from Folders to resolve from, in order
|
|
22
|
-
* @returns {Promise<
|
|
125
|
+
* @returns {Promise<Record<string, unknown>>} The module namespace
|
|
23
126
|
* @throws When no folder resolves it, saying which devDependency to add
|
|
24
127
|
*/
|
|
25
|
-
|
|
128
|
+
async function importNamespace(name, from) {
|
|
26
129
|
for (const folder of from) {
|
|
27
130
|
try {
|
|
28
131
|
const resolved = createRequire(join(folder, "package.json")).resolve(
|
|
29
132
|
name,
|
|
30
133
|
);
|
|
31
|
-
|
|
134
|
+
/** @type {unknown} */
|
|
135
|
+
const namespace = await import(pathToFileURL(resolved).href);
|
|
136
|
+
return withDefaultExports(namespace);
|
|
32
137
|
} catch {
|
|
33
138
|
continue;
|
|
34
139
|
}
|
|
@@ -38,6 +143,20 @@ export async function importFrom(name, from) {
|
|
|
38
143
|
);
|
|
39
144
|
}
|
|
40
145
|
|
|
146
|
+
/**
|
|
147
|
+
* A package's namespace, for the one caller whose use of it cannot be checked
|
|
148
|
+
* at the import: it launches Playwright's browser and drives it through
|
|
149
|
+
* Playwright's own declarations.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} name The package name
|
|
152
|
+
* @param {string[]} from Folders to resolve from, in order
|
|
153
|
+
* @returns {Promise<any>} The module namespace
|
|
154
|
+
* @throws When no folder resolves it, saying which devDependency to add
|
|
155
|
+
*/
|
|
156
|
+
export async function importFrom(name, from) {
|
|
157
|
+
return importNamespace(name, from);
|
|
158
|
+
}
|
|
159
|
+
|
|
41
160
|
function mongodOnPath() {
|
|
42
161
|
return spawnSync("mongod", ["--version"], { stdio: "ignore" }).status === 0
|
|
43
162
|
? "mongod"
|
|
@@ -51,11 +170,21 @@ function mongodOnPath() {
|
|
|
51
170
|
* @param {string[]} from Folders to resolve mongodb-memory-server-core from
|
|
52
171
|
* @returns {Promise<string>} The binary path
|
|
53
172
|
*/
|
|
54
|
-
async function mongodBinary(from) {
|
|
173
|
+
export async function mongodBinary(from) {
|
|
55
174
|
const configured = process.env.DMS_CONTROL_MONGOD ?? mongodOnPath();
|
|
56
175
|
if (configured) return configured;
|
|
57
|
-
const
|
|
58
|
-
|
|
176
|
+
const namespace = await importNamespace("mongodb-memory-server-core", from);
|
|
177
|
+
const binary = namespace.MongoBinary;
|
|
178
|
+
if (!isBinaryResolver(binary))
|
|
179
|
+
throw new TypeError("mongodb-memory-server-core exports no MongoBinary");
|
|
180
|
+
/** @type {unknown} */
|
|
181
|
+
const path = await binary.getPath({ version: MONGO_VERSION });
|
|
182
|
+
return decode(
|
|
183
|
+
isString,
|
|
184
|
+
path,
|
|
185
|
+
"MongoBinary.getPath",
|
|
186
|
+
"the path of a mongod binary",
|
|
187
|
+
);
|
|
59
188
|
}
|
|
60
189
|
|
|
61
190
|
/**
|
|
@@ -89,6 +218,10 @@ export async function startMongod(paths, from) {
|
|
|
89
218
|
return { url: `mongodb://127.0.0.1:${port}`, record: { ...record, port } };
|
|
90
219
|
}
|
|
91
220
|
|
|
221
|
+
/**
|
|
222
|
+
* @param {string[]} from
|
|
223
|
+
* @returns {string | undefined}
|
|
224
|
+
*/
|
|
92
225
|
function driverBesideMemoryServer(from) {
|
|
93
226
|
for (const folder of from) {
|
|
94
227
|
try {
|
|
@@ -103,11 +236,56 @@ function driverBesideMemoryServer(from) {
|
|
|
103
236
|
return undefined;
|
|
104
237
|
}
|
|
105
238
|
|
|
239
|
+
/**
|
|
240
|
+
* @param {string[]} from
|
|
241
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
242
|
+
*/
|
|
106
243
|
async function mongoDriver(from) {
|
|
107
244
|
const beside = driverBesideMemoryServer(from);
|
|
108
245
|
if (beside)
|
|
109
246
|
return withDefaultExports(await import(pathToFileURL(beside).href));
|
|
110
|
-
return
|
|
247
|
+
return importNamespace("mongodb", from);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {Client} client A driver client
|
|
252
|
+
* @param {string} name The database
|
|
253
|
+
* @returns {Database} Its handle
|
|
254
|
+
* @throws When the driver answers anything but a database
|
|
255
|
+
*/
|
|
256
|
+
function databaseOf(client, name) {
|
|
257
|
+
/** @type {unknown} */
|
|
258
|
+
const database = client.db(name);
|
|
259
|
+
if (!isDatabase(database))
|
|
260
|
+
throw new TypeError(`MongoClient.db(${name}) returned no database`);
|
|
261
|
+
return database;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Runs `use` on a client connected to the server, then closes it.
|
|
266
|
+
*
|
|
267
|
+
* @template T
|
|
268
|
+
* @param {string} url The server URL
|
|
269
|
+
* @param {string[]} from Folders to resolve the driver from
|
|
270
|
+
* @param {(client: Client) => Promise<T>} use What to do with the connected client
|
|
271
|
+
* @returns {Promise<T>} What `use` answered
|
|
272
|
+
* @throws When the driver exports no `MongoClient` that builds a client
|
|
273
|
+
*/
|
|
274
|
+
async function withClient(url, from, use) {
|
|
275
|
+
const namespace = await mongoDriver(from);
|
|
276
|
+
const MongoClient = namespace.MongoClient;
|
|
277
|
+
if (!isConstructor(MongoClient))
|
|
278
|
+
throw new TypeError("The MongoDB driver exports no MongoClient");
|
|
279
|
+
/** @type {unknown} */
|
|
280
|
+
const client = new MongoClient(url, { serverSelectionTimeoutMS: 5_000 });
|
|
281
|
+
if (!isClient(client))
|
|
282
|
+
throw new TypeError("MongoClient built something that is no client");
|
|
283
|
+
try {
|
|
284
|
+
await client.connect();
|
|
285
|
+
return await use(client);
|
|
286
|
+
} finally {
|
|
287
|
+
await client.close();
|
|
288
|
+
}
|
|
111
289
|
}
|
|
112
290
|
|
|
113
291
|
/**
|
|
@@ -120,20 +298,25 @@ async function mongoDriver(from) {
|
|
|
120
298
|
* @param {string[]} from Folders to resolve the driver from
|
|
121
299
|
* @returns {Promise<string[]>} One line per dropped database
|
|
122
300
|
*/
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
301
|
+
export function dropRunDatabases(url, prefix, from) {
|
|
302
|
+
return withClient(url, from, async (client) => {
|
|
303
|
+
const admin = databaseOf(client, "admin").admin();
|
|
304
|
+
if (!isAdmin(admin))
|
|
305
|
+
throw new TypeError(
|
|
306
|
+
"The admin handle of the driver has no listDatabases",
|
|
307
|
+
);
|
|
308
|
+
const { databases } = decode(
|
|
309
|
+
isDatabaseList,
|
|
310
|
+
await admin.listDatabases(),
|
|
311
|
+
"listDatabases",
|
|
312
|
+
"a list of databases",
|
|
313
|
+
);
|
|
129
314
|
const owned = databases
|
|
130
315
|
.map((database) => database.name)
|
|
131
316
|
.filter((name) => name.startsWith(prefix));
|
|
132
|
-
for (const name of owned) await client
|
|
317
|
+
for (const name of owned) await databaseOf(client, name).dropDatabase();
|
|
133
318
|
return owned.map((name) => `dropped database ${name}`);
|
|
134
|
-
}
|
|
135
|
-
await client.close();
|
|
136
|
-
}
|
|
319
|
+
});
|
|
137
320
|
}
|
|
138
321
|
|
|
139
322
|
/**
|
|
@@ -144,13 +327,8 @@ export async function dropRunDatabases(url, prefix, from) {
|
|
|
144
327
|
* @param {string[]} from Folders to resolve the driver from
|
|
145
328
|
* @returns {Promise<void>} Resolves once dropped
|
|
146
329
|
*/
|
|
147
|
-
export
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await client.connect();
|
|
152
|
-
await client.db(name).dropDatabase();
|
|
153
|
-
} finally {
|
|
154
|
-
await client.close();
|
|
155
|
-
}
|
|
330
|
+
export function dropDatabase(url, name, from) {
|
|
331
|
+
return withClient(url, from, async (client) => {
|
|
332
|
+
await databaseOf(client, name).dropDatabase();
|
|
333
|
+
});
|
|
156
334
|
}
|