├── packages ├── data │ ├── src │ │ ├── index.ts │ │ └── stub.test.ts │ ├── readme.md │ ├── tsconfig.prod.json │ └── tsconfig.json ├── data-driver-postgres │ ├── src │ │ ├── index.ts │ │ └── stub.test.ts │ ├── readme.md │ ├── tsconfig.prod.json │ └── tsconfig.json ├── sdk │ ├── index.mjs │ ├── tsconfig.prod.json │ ├── src │ │ ├── base │ │ │ ├── storage │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── singleton.ts │ │ ├── index.ts │ │ ├── storage.ts │ │ └── handlers │ │ │ ├── roles.ts │ │ │ ├── folders.ts │ │ │ ├── presets.ts │ │ │ └── revisions.ts │ ├── tests │ │ ├── tsconfig.json │ │ ├── base │ │ │ ├── storage │ │ │ │ ├── memory.test.ts │ │ │ │ └── localstorage.test.ts │ │ │ └── directus.node.ts │ │ └── blog.d.ts │ └── vitest.config.ts ├── extensions-sdk │ ├── cli.d.ts │ ├── cli.js │ ├── templates │ │ ├── common │ │ │ └── common │ │ │ │ └── config │ │ │ │ └── _gitignore │ │ ├── endpoint │ │ │ ├── javascript │ │ │ │ └── source │ │ │ │ │ └── index.js │ │ │ └── typescript │ │ │ │ └── source │ │ │ │ └── index.ts │ │ ├── operation │ │ │ ├── javascript │ │ │ │ └── source │ │ │ │ │ ├── api.js │ │ │ │ │ └── app.js │ │ │ └── typescript │ │ │ │ └── source │ │ │ │ ├── shims.d.ts │ │ │ │ └── api.ts │ │ ├── display │ │ │ ├── typescript │ │ │ │ └── source │ │ │ │ │ ├── shims.d.ts │ │ │ │ │ ├── display.vue │ │ │ │ │ └── index.ts │ │ │ └── javascript │ │ │ │ └── source │ │ │ │ ├── display.vue │ │ │ │ └── index.js │ │ ├── layout │ │ │ ├── typescript │ │ │ │ └── source │ │ │ │ │ ├── shims.d.ts │ │ │ │ │ └── layout.vue │ │ │ └── javascript │ │ │ │ └── source │ │ │ │ ├── layout.vue │ │ │ │ └── index.js │ │ ├── module │ │ │ ├── typescript │ │ │ │ └── source │ │ │ │ │ ├── shims.d.ts │ │ │ │ │ ├── module.vue │ │ │ │ │ └── index.ts │ │ │ └── javascript │ │ │ │ └── source │ │ │ │ ├── module.vue │ │ │ │ └── index.js │ │ ├── panel │ │ │ ├── typescript │ │ │ │ └── source │ │ │ │ │ └── shims.d.ts │ │ │ └── javascript │ │ │ │ └── source │ │ │ │ └── index.js │ │ ├── interface │ │ │ ├── typescript │ │ │ │ └── source │ │ │ │ │ ├── shims.d.ts │ │ │ │ │ └── index.ts │ │ │ └── javascript │ │ │ │ └── source │ │ │ │ ├── index.js │ │ │ │ └── interface.vue │ │ └── hook │ │ │ ├── javascript │ │ │ └── source │ │ │ │ └── index.js │ │ │ └── typescript │ │ │ └── source │ │ │ └── index.ts │ ├── tsconfig.prod.json │ ├── src │ │ ├── cli │ │ │ ├── utils │ │ │ │ ├── file.ts │ │ │ │ ├── try-parse-json.ts │ │ │ │ ├── get-template-path.ts │ │ │ │ ├── get-sdk-version.test.ts │ │ │ │ ├── get-package-version.ts │ │ │ │ ├── get-sdk-version.ts │ │ │ │ └── detect-json-indent.ts │ │ │ └── index.ts │ │ └── index.ts │ └── tsconfig.json ├── utils │ ├── browser │ │ ├── index.ts │ │ ├── tsconfig.prod.json │ │ ├── tsconfig.json │ │ └── css-var.ts │ ├── readme.md │ ├── node │ │ ├── tsconfig.prod.json │ │ ├── tsconfig.json │ │ ├── path-to-relative-url.ts │ │ ├── pluralize.ts │ │ ├── resolve-package.ts │ │ ├── readable-stream-to-string.ts │ │ ├── is-readable-stream.ts │ │ ├── pluralize.test.ts │ │ ├── array-helpers.ts │ │ ├── index.ts │ │ └── readable-stream-to-string.test.ts │ ├── shared │ │ ├── tsconfig.prod.json │ │ ├── tsconfig.json │ │ ├── to-array.ts │ │ ├── get-endpoint.ts │ │ ├── is-object.ts │ │ ├── is-valid-json.ts │ │ ├── is-dynamic-variable.ts │ │ ├── pluralize.ts │ │ ├── pluralize.test.ts │ │ ├── get-fields-from-template.ts │ │ ├── get-simple-hash.ts │ │ ├── array-helpers.ts │ │ ├── merge-filters.ts │ │ ├── parse-json.ts │ │ ├── to-array.test.ts │ │ ├── add-field-flag.ts │ │ ├── get-endpoint.test.ts │ │ ├── get-simple-hash.test.ts │ │ ├── get-collection-type.ts │ │ ├── get-output-type-for-function.ts │ │ ├── is-valid-json.test.ts │ │ └── get-fields-from-template.test.ts │ └── tsconfig.json ├── constants │ ├── src │ │ ├── log.ts │ │ ├── regex.ts │ │ ├── files.ts │ │ ├── injection.ts │ │ ├── activity.ts │ │ └── index.ts │ ├── tsconfig.prod.json │ └── tsconfig.json ├── specs │ ├── src │ │ ├── components │ │ │ └── item.yaml │ │ ├── parameters │ │ │ ├── id.yaml │ │ │ ├── meta.yaml │ │ │ ├── offset.yaml │ │ │ ├── limit.yaml │ │ │ ├── collection.yaml │ │ │ ├── page.yaml │ │ │ ├── search.yaml │ │ │ ├── uuid.yaml │ │ │ ├── mode.yaml │ │ │ ├── fields.yaml │ │ │ ├── export.yaml │ │ │ ├── filter.yaml │ │ │ └── sort.yaml │ │ ├── paths │ │ │ ├── utils │ │ │ │ └── cache-clear.yaml │ │ │ ├── server │ │ │ │ └── ping.yaml │ │ │ └── users │ │ │ │ ├── me-tfa-enable.yaml │ │ │ │ └── me-tfa-disable.yaml │ │ └── responses │ │ │ ├── notFoundError.yaml │ │ │ └── unauthorizedError.yaml │ ├── index.d.ts │ └── index.js ├── pressure │ ├── src │ │ └── index.ts │ ├── tsconfig.prod.json │ └── tsconfig.json ├── random │ ├── src │ │ ├── constants.ts │ │ ├── uuid.ts │ │ ├── index.ts │ │ ├── integer.ts │ │ ├── array.ts │ │ ├── alpha.ts │ │ ├── uuid.test.ts │ │ ├── integer.test.ts │ │ └── sequence.ts │ ├── readme.md │ └── tsconfig.json ├── storage │ ├── readme.md │ ├── tsconfig.prod.json │ └── tsconfig.json ├── exceptions │ ├── src │ │ ├── index.ts │ │ └── base.ts │ ├── tsconfig.prod.json │ └── tsconfig.json ├── types │ ├── tsconfig.prod.json │ ├── src │ │ ├── items.ts │ │ ├── vue.ts │ │ ├── notifications.ts │ │ ├── shares.ts │ │ ├── modules.ts │ │ ├── accountability.ts │ │ ├── permissions.ts │ │ └── presets.ts │ └── tsconfig.json ├── schema │ ├── tsconfig.prod.json │ ├── tsconfig.json │ └── src │ │ ├── utils │ │ ├── extract-type.ts │ │ ├── extract-max-length.ts │ │ └── strip-quotes.ts │ │ └── types │ │ ├── table.ts │ │ ├── foreign-key.ts │ │ └── overview.ts ├── composables │ ├── tsconfig.prod.json │ ├── tsconfig.json │ └── src │ │ ├── index.ts │ │ └── use-sync.ts ├── update-check │ ├── tsconfig.prod.json │ └── tsconfig.json ├── storage-driver-azure │ ├── tsconfig.prod.json │ ├── readme.md │ └── tsconfig.json ├── storage-driver-gcs │ ├── tsconfig.prod.json │ ├── readme.md │ └── tsconfig.json ├── storage-driver-local │ ├── tsconfig.prod.json │ ├── readme.md │ └── tsconfig.json ├── storage-driver-s3 │ ├── readme.md │ ├── tsconfig.prod.json │ └── tsconfig.json ├── release-notes-generator │ ├── tsconfig.prod.json │ └── tsconfig.json ├── storage-driver-cloudinary │ ├── tsconfig.prod.json │ ├── readme.md │ └── tsconfig.json ├── tsconfig │ └── readme.md └── create-directus-extension │ └── readme.md ├── readme.md ├── app ├── stub │ └── empty.d.ts ├── src │ ├── lang │ │ ├── translations │ │ │ ├── kmr-TR.yaml │ │ │ ├── sw-KE.yaml │ │ │ ├── sw-TZ.yaml │ │ │ ├── tg-TJ.yaml │ │ │ └── si-LK.yaml │ │ └── number-formats.yaml │ ├── views │ │ ├── private │ │ │ ├── readme.md │ │ │ ├── index.ts │ │ │ └── components │ │ │ │ └── value-null.vue │ │ ├── public │ │ │ └── index.ts │ │ ├── readme.md │ │ └── register.ts │ ├── vite-env.d.ts │ ├── assets │ │ ├── readme.md │ │ └── fonts │ │ │ ├── Inter-Black.woff │ │ │ ├── Inter-Bold.woff │ │ │ ├── Inter-Bold.woff2 │ │ │ ├── Inter-Black.woff2 │ │ │ ├── Inter-Medium.woff │ │ │ ├── Inter-Medium.woff2 │ │ │ ├── Inter-Regular.woff │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── Inter-SemiBold.woff │ │ │ ├── FiraMono-Medium.woff │ │ │ ├── FiraMono-Medium.woff2 │ │ │ ├── Inter-SemiBold.woff2 │ │ │ ├── Inter-MediumItalic.woff │ │ │ ├── Inter-MediumItalic.woff2 │ │ │ ├── material-symbols.woff2 │ │ │ ├── merriweather-bold.woff │ │ │ ├── merriweather-bold.woff2 │ │ │ ├── merriweather-italic.woff │ │ │ ├── merriweather-light.woff │ │ │ ├── merriweather-light.woff2 │ │ │ ├── merriweather-italic.woff2 │ │ │ ├── merriweather-regular.woff │ │ │ └── merriweather-regular.woff2 │ ├── types │ │ ├── interfaces.ts │ │ ├── login.ts │ │ ├── error.ts │ │ ├── folders.ts │ │ ├── insights.ts │ │ ├── collections.ts │ │ ├── activity.ts │ │ ├── panels.ts │ │ └── notifications.ts │ ├── styles │ │ └── mixins │ │ │ └── no-wrap.scss │ ├── __utils__ │ │ ├── tooltip.ts │ │ ├── crypto.ts │ │ ├── focus.ts │ │ └── router.ts │ ├── events.ts │ ├── components │ │ ├── __snapshots__ │ │ │ ├── v-error-boundary.test.ts.snap │ │ │ ├── v-hover.test.ts.snap │ │ │ ├── v-avatar.test.ts.snap │ │ │ ├── v-card.test.ts.snap │ │ │ ├── v-sheet.test.ts.snap │ │ │ ├── v-tabs.test.ts.snap │ │ │ ├── v-card-text.test.ts.snap │ │ │ ├── v-text-overflow.test.ts.snap │ │ │ ├── v-card-actions.test.ts.snap │ │ │ ├── v-card-subtitle.test.ts.snap │ │ │ ├── v-card-title.test.ts.snap │ │ │ ├── v-chip.test.ts.snap │ │ │ ├── v-badge.test.ts.snap │ │ │ ├── v-notice.test.ts.snap │ │ │ ├── v-textarea.test.ts.snap │ │ │ ├── v-progress-linear.test.ts.snap │ │ │ ├── v-menu.test.ts.snap │ │ │ ├── v-icon-file.test.ts.snap │ │ │ ├── v-skeleton-loader.test.ts.snap │ │ │ ├── v-overlay.test.ts.snap │ │ │ ├── v-highlight.test.ts.snap │ │ │ ├── v-info.test.ts.snap │ │ │ ├── v-radio.test.ts.snap │ │ │ ├── v-divider.test.ts.snap │ │ │ └── v-button.test.ts.snap │ │ ├── v-field-template │ │ │ └── types.ts │ │ ├── v-checkbox-tree │ │ │ └── __snapshots__ │ │ │ │ └── v-checkbox-tree.test.ts.snap │ │ ├── v-form │ │ │ └── types.ts │ │ ├── v-card-subtitle.vue │ │ ├── v-card-text.vue │ │ ├── v-select │ │ │ ├── types.ts │ │ │ └── __snapshots__ │ │ │ │ └── v-select.test.ts.snap │ │ ├── v-icon │ │ │ ├── __snapshots__ │ │ │ │ └── v-icon.test.ts.snap │ │ │ └── custom-icons │ │ │ │ ├── grid_1.vue │ │ │ │ ├── grid_2.vue │ │ │ │ ├── flip_horizontal.vue │ │ │ │ └── grid_3.vue │ │ ├── v-card-actions.vue │ │ ├── v-card-title.vue │ │ ├── v-sheet.test.ts │ │ ├── v-card-text.test.ts │ │ ├── v-card-title.test.ts │ │ ├── v-card-actions.test.ts │ │ ├── v-card-subtitle.test.ts │ │ ├── v-icon-file.test.ts │ │ ├── v-divider.stories.ts │ │ ├── v-icon.stories.ts │ │ ├── v-avatar.stories.ts │ │ └── v-chip.stories.ts │ ├── routes │ │ ├── login │ │ │ └── components │ │ │ │ └── login-form │ │ │ │ └── index.ts │ │ └── logout.vue │ ├── utils │ │ ├── percentage.ts │ │ ├── is-hex.ts │ │ ├── router-passthrough.ts │ │ ├── hide-drag-image.ts │ │ ├── translate-literal.ts │ │ ├── notify.ts │ │ ├── jwt-payload.ts │ │ ├── readable-mime-type │ │ │ └── index.ts │ │ ├── is-permission-empty.ts │ │ ├── get-special-for-type.ts │ │ ├── get-asset-url.ts │ │ ├── hide-drag-image.test.ts │ │ ├── sync-ref-property.ts │ │ ├── localized-format.ts │ │ ├── localized-format-distance.ts │ │ └── extract-field-from-function.test.ts │ ├── layouts │ │ ├── calendar │ │ │ └── types.ts │ │ ├── cards │ │ │ └── types.ts │ │ ├── tabular │ │ │ └── types.ts │ │ └── map │ │ │ └── types.ts │ ├── directives │ │ └── focus.ts │ ├── interfaces │ │ ├── presentation-links │ │ │ └── preview.svg │ │ ├── _system │ │ │ ├── system-field │ │ │ │ └── index.ts │ │ │ ├── system-mfa-setup │ │ │ │ └── index.ts │ │ │ ├── system-field-tree │ │ │ │ └── index.ts │ │ │ ├── system-filter │ │ │ │ └── index.ts │ │ │ ├── system-scope │ │ │ │ └── index.ts │ │ │ ├── system-language │ │ │ │ └── index.ts │ │ │ ├── system-fields │ │ │ │ └── index.ts │ │ │ ├── system-modules │ │ │ │ └── index.ts │ │ │ ├── system-inline-fields │ │ │ │ └── index.ts │ │ │ ├── system-token │ │ │ │ └── index.ts │ │ │ ├── system-interface │ │ │ │ └── index.ts │ │ │ └── system-folder │ │ │ │ └── index.ts │ │ ├── presentation-divider │ │ │ └── preview.svg │ │ ├── group-detail │ │ │ └── preview.svg │ │ └── input-autocomplete-api │ │ │ └── preview.svg │ ├── displays │ │ ├── raw │ │ │ └── index.ts │ │ └── file │ │ │ └── index.ts │ └── modules │ │ └── settings │ │ └── routes │ │ ├── flows │ │ └── constants.ts │ │ ├── data-model │ │ └── field-detail │ │ │ └── store │ │ │ ├── types.ts │ │ │ └── alterations │ │ │ └── index.ts │ │ └── project │ │ └── components │ │ └── project-info-sidebar-detail.vue ├── .storybook │ ├── styles.scss │ └── preview-body.html ├── public │ ├── favicon.ico │ ├── img │ │ ├── directus-white.png │ │ └── icons │ │ │ └── mstile-150x150.png │ ├── manifest.webmanifest │ └── browserconfig.xml └── readme.md ├── docs ├── index.md ├── public │ └── icons │ │ ├── netlify.webp │ │ ├── nextjs.png │ │ ├── vercel.svg │ │ └── card_link.svg ├── .spellcheckerrc.yml ├── app │ ├── security.md │ └── faq.md └── guides │ └── headless-cms │ └── schedule-content │ └── index.md ├── .npmrc ├── tests └── blackbox │ ├── schema │ ├── csv │ │ └── index.ts │ ├── hash │ │ └── index.ts │ ├── text │ │ └── index.ts │ ├── time │ │ └── index.ts │ ├── date │ │ └── index.ts │ ├── decimal │ │ └── index.ts │ └── timestamp │ │ └── index.ts │ ├── query │ └── index.ts │ ├── common │ ├── types.ts │ └── index.ts │ ├── assets │ ├── layers.png │ ├── directus.avif │ └── directus.png │ ├── setup │ ├── seeds │ │ ├── 03_tests_extensions.js │ │ └── 01_delete_existing_data.js │ ├── global.ts │ ├── sequentialTests.d.ts │ └── migrations │ │ └── 20221107_add_extensions_testing.js │ ├── utils │ ├── sleep.ts │ └── validate-date-difference.ts │ └── tsconfig.json ├── directus └── cli.js ├── .eslintignore ├── api ├── src │ ├── start.ts │ ├── types │ │ ├── meta.ts │ │ ├── migration.ts │ │ ├── revision.ts │ │ ├── database.ts │ │ ├── webhooks.ts │ │ ├── graphql.ts │ │ ├── events.ts │ │ ├── express.d.ts │ │ └── index.ts │ ├── index.ts │ ├── database │ │ ├── helpers │ │ │ ├── date │ │ │ │ └── dialects │ │ │ │ │ ├── default.ts │ │ │ │ │ ├── oracle.ts │ │ │ │ │ └── mssql.ts │ │ │ ├── schema │ │ │ │ └── dialects │ │ │ │ │ └── default.ts │ │ │ ├── types.ts │ │ │ └── fn │ │ │ │ └── index.ts │ │ ├── system-data │ │ │ └── fields │ │ │ │ ├── folders.yaml │ │ │ │ ├── _defaults.yaml │ │ │ │ ├── migrations.yaml │ │ │ │ ├── sessions.yaml │ │ │ │ ├── notifications.yaml │ │ │ │ ├── dashboards.yaml │ │ │ │ └── revisions.yaml │ │ ├── seeds │ │ │ ├── 15-migrations.yaml │ │ │ ├── 06-folders.yaml │ │ │ └── 12-sessions.yaml │ │ └── migrations │ │ │ ├── 20210422A-remove-files-interface.ts │ │ │ ├── 20220614A-rename-hook-trigger-to-event.ts │ │ │ ├── 20211009A-add-auth-data.ts │ │ │ ├── 20211016A-add-webhook-headers.ts │ │ │ ├── 20211103A-set-unique-to-user-token.ts │ │ │ ├── 20210716A-add-conditions-to-fields.ts │ │ │ ├── 20220429C-drop-non-null-from-ip-of-activity.ts │ │ │ ├── 20210831A-remove-limit-column.ts │ │ │ ├── 20211104A-remove-collections-listing.ts │ │ │ ├── 20210331A-add-refresh-interval.ts │ │ │ ├── 20210521A-add-collections-icon-color.ts │ │ │ ├── 20210803A-add-required-to-fields.ts │ │ │ ├── 20220429B-add-color-to-insights-icon.ts │ │ │ ├── 20220314A-add-translation-strings.ts │ │ │ ├── 20210304A-remove-locked-fields.ts │ │ │ ├── 20220802A-add-custom-aspect-ratios.ts │ │ │ ├── 20211230A-add-project-descriptor.ts │ │ │ ├── 20220429D-drop-non-null-from-sender-of-notifications.ts │ │ │ ├── 20210929A-rename-login-action.ts │ │ │ └── 20210608A-add-deep-clone-config.ts │ ├── auth │ │ └── drivers │ │ │ └── index.ts │ ├── cli │ │ ├── commands │ │ │ ├── security │ │ │ │ ├── key.ts │ │ │ │ └── secret.ts │ │ │ └── database │ │ │ │ └── install.ts │ │ ├── run.ts │ │ └── utils │ │ │ └── defaults.ts │ ├── utils │ │ ├── get-module-default.ts │ │ ├── get-string-byte-size.ts │ │ ├── sanitize-error.ts │ │ ├── get-versioned-hash.ts │ │ ├── require-yaml.ts │ │ ├── md.ts │ │ ├── async-handler.ts │ │ ├── get-graphql-query-and-variables.ts │ │ ├── get-string-byte-size.test.ts │ │ ├── strip-function.test.ts │ │ ├── strip-function.ts │ │ ├── get-date-formatted.ts │ │ ├── validate-env.ts │ │ ├── package.ts │ │ ├── md.test.ts │ │ ├── is-directus-jwt.ts │ │ ├── user-name.ts │ │ ├── get-milliseconds.ts │ │ └── get-collection-from-alias.ts │ ├── services │ │ ├── graphql │ │ │ └── types │ │ │ │ ├── date.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── geojson.ts │ │ │ │ └── void.ts │ │ ├── panels.ts │ │ ├── folders.ts │ │ ├── presets.ts │ │ ├── settings.ts │ │ ├── dashboards.ts │ │ └── mail │ │ │ └── templates │ │ │ └── user-invitation.liquid │ ├── exceptions │ │ ├── invalid-query.ts │ │ ├── invalid-ip.ts │ │ ├── invalid-otp.ts │ │ ├── invalid-token.ts │ │ ├── token-expired.ts │ │ ├── unexpected-response.ts │ │ ├── user-suspended.ts │ │ ├── invalid-provider.ts │ │ ├── unprocessable-entity.ts │ │ ├── route-not-found.ts │ │ ├── illegal-asset-transformation.ts │ │ ├── invalid-credentials.ts │ │ ├── invalid-payload.ts │ │ ├── invalid-config.ts │ │ ├── unsupported-media-type.ts │ │ ├── graphql-validation.ts │ │ ├── hit-rate-limit.ts │ │ ├── method-not-allowed.ts │ │ ├── service-unavailable.ts │ │ └── database │ │ │ ├── not-null-violation.ts │ │ │ └── contains-null-values.ts │ ├── request │ │ └── response-interceptor.ts │ ├── operations │ │ ├── transform │ │ │ └── index.ts │ │ ├── log │ │ │ └── index.ts │ │ └── sleep │ │ │ └── index.ts │ ├── middleware │ │ ├── schema.ts │ │ └── use-collection.ts │ └── __mocks__ │ │ └── cache.mts ├── tsconfig.prod.json ├── tsconfig.json ├── vitest.config.ts └── globalSetup.js ├── .github ├── CODEOWNERS ├── codeql │ └── codeql-config.yml ├── FUNDING.yml └── workflows │ └── slash-commands.yaml ├── .prettierignore ├── .changeset ├── strange-ears-pull.md ├── itchy-rockets-confess.md ├── olive-toys-fail.md ├── stupid-pumas-care.md ├── warm-pumas-provide.md ├── wet-beds-fold.md ├── pretty-lobsters-look.md ├── tough-crews-shout.md ├── moody-poems-pump.md ├── hip-crabs-rhyme.md ├── stupid-pens-divide.md ├── cool-jobs-smoke.md ├── friendly-rocks-sell.md ├── giant-ligers-nail.md └── config.json ├── pnpm-workspace.yaml ├── crowdin.yml ├── .prettierrc.js ├── .dockerignore ├── code_of_conduct.md ├── contributing.md └── .editorconfig /packages/data/src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | directus/readme.md -------------------------------------------------------------------------------- /app/stub/empty.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/data-driver-postgres/src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/lang/translations/kmr-TR.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /app/src/lang/translations/sw-KE.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /app/src/lang/translations/sw-TZ.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /app/src/lang/translations/tg-TJ.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | getting-started/introduction.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /app/src/views/private/readme.md: -------------------------------------------------------------------------------- 1 | # Private View 2 | -------------------------------------------------------------------------------- /packages/sdk/index.mjs: -------------------------------------------------------------------------------- 1 | export * from './dist/sdk.cjs.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions-sdk/cli.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/esm/cli'; 2 | -------------------------------------------------------------------------------- /packages/utils/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './css-var.js'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/csv/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../string'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/hash/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../string'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../string'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/time/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../date'; 2 | -------------------------------------------------------------------------------- /app/.storybook/styles.scss: -------------------------------------------------------------------------------- 1 | .active { 2 | color: var(--primary); 3 | } -------------------------------------------------------------------------------- /app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DIRECTUS_VERSION__: string; 2 | -------------------------------------------------------------------------------- /tests/blackbox/query/index.ts: -------------------------------------------------------------------------------- 1 | export * as filter from './filter'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/date/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../date-time'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/decimal/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../integer'; 2 | -------------------------------------------------------------------------------- /tests/blackbox/schema/timestamp/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../date'; 2 | -------------------------------------------------------------------------------- /directus/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import '@directus/api/cli/run.js'; 3 | -------------------------------------------------------------------------------- /packages/constants/src/log.ts: -------------------------------------------------------------------------------- 1 | export const REDACTED_TEXT = '--redacted--'; 2 | -------------------------------------------------------------------------------- /packages/specs/src/components/item.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: {} 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | packages/extensions-sdk/templates 4 | extensions -------------------------------------------------------------------------------- /api/src/start.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from './server.js'; 2 | 3 | startServer(); 4 | -------------------------------------------------------------------------------- /packages/constants/src/regex.ts: -------------------------------------------------------------------------------- 1 | export const REGEX_BETWEEN_PARENS = /\(([^)]+)\)/; 2 | -------------------------------------------------------------------------------- /packages/data/readme.md: -------------------------------------------------------------------------------- 1 | # `@directus/data` 2 | 3 | Data abstraction for Directus 4 | -------------------------------------------------------------------------------- /packages/extensions-sdk/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import './dist/cli/run.js'; 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /docs/**/*.md @phazonoverload 2 | /docs/.vitepress/* @phazonoverload 3 | -------------------------------------------------------------------------------- /app/.storybook/preview-body.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/directus/main/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/pressure/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './monitor.js'; 2 | export * from './express.js'; 3 | -------------------------------------------------------------------------------- /packages/random/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CHARACTERS_ALPHA = 'abcdefghijklmnopqrstuvwxyz'; 2 | -------------------------------------------------------------------------------- /packages/storage/readme.md: -------------------------------------------------------------------------------- 1 | # `@directus/storage` 2 | 3 | Storage abstraction for Directus 4 | -------------------------------------------------------------------------------- /tests/blackbox/common/types.ts: -------------------------------------------------------------------------------- 1 | export type PrimaryKeyType = 'integer' | 'uuid' | 'string'; 2 | -------------------------------------------------------------------------------- /api/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/constants/src/files.ts: -------------------------------------------------------------------------------- 1 | export const JAVASCRIPT_FILE_EXTS = ['js', 'mjs', 'cjs'] as const; 2 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/common/common/config/_gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /packages/exceptions/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js'; 2 | export * from './failed-validation.js'; 3 | -------------------------------------------------------------------------------- /packages/utils/readme.md: -------------------------------------------------------------------------------- 1 | # `@directus/utils` 2 | 3 | Utilities shared between the Directus packages 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | pnpm-lock.yaml 5 | app/src/lang/translations/*.yaml 6 | -------------------------------------------------------------------------------- /docs/public/icons/netlify.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/directus/main/docs/public/icons/netlify.webp -------------------------------------------------------------------------------- /docs/public/icons/nextjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/directus/main/docs/public/icons/nextjs.png -------------------------------------------------------------------------------- /packages/data/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/types/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/.spellcheckerrc.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - '**/*.md' 3 | - '!node_modules' 4 | dictionaries: 5 | - dictionary.txt 6 | -------------------------------------------------------------------------------- /packages/constants/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/data-driver-postgres/readme.md: -------------------------------------------------------------------------------- 1 | # `@directus/data-driver-postgres` 2 | 3 | Data abstraction for Postgres 4 | -------------------------------------------------------------------------------- /packages/exceptions/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/pressure/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/schema/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/specs/index.d.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIObject } from 'openapi3-ts'; 2 | 3 | export const spec: OpenAPIObject; 4 | -------------------------------------------------------------------------------- /packages/storage/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/utils/node/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/blackbox/assets/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/directus/main/tests/blackbox/assets/layers.png -------------------------------------------------------------------------------- /.changeset/strange-ears-pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | "docs": patch 3 | --- 4 | 5 | Improved Affordance of Docs Card Component Link 6 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - '**/*.test.ts' 3 | - '**/*.test.js' 4 | - '**/node_modules' 5 | -------------------------------------------------------------------------------- /api/src/types/meta.ts: -------------------------------------------------------------------------------- 1 | export enum Meta { 2 | TOTAL_COUNT = 'total_count', 3 | FILTER_COUNT = 'filter_count', 4 | } 5 | -------------------------------------------------------------------------------- /app/public/img/directus-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/download/directus/main/app/public/img/directus-white.png -------------------------------------------------------------------------------- /app/src/assets/readme.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | Static assets that are used in the app. Primarily used for logos and fonts. 4 | -------------------------------------------------------------------------------- /packages/composables/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/extensions-sdk/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/types/src/items.ts: -------------------------------------------------------------------------------- 1 | export type Item = Record>(query?: Q): Promise>; 8 | update >(item: ItemInput, query?: Q): Promise >; 9 | } 10 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/hook/typescript/source/index.ts: -------------------------------------------------------------------------------- 1 | import { defineHook } from '@directus/extensions-sdk'; 2 | 3 | export default defineHook(({ filter, action }) => { 4 | filter('items.create', () => { 5 | console.log('Creating Item!'); 6 | }); 7 | 8 | action('items.create', () => { 9 | console.log('Item created!'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/types/src/notifications.ts: -------------------------------------------------------------------------------- 1 | import type { PrimaryKey } from './items.js'; 2 | 3 | export type Notification = { 4 | id: string; 5 | status: string; 6 | timestamp: string; 7 | recipient: string; 8 | sender: string | null; 9 | subject: string; 10 | message: string | null; 11 | collection: string | null; 12 | item: PrimaryKey | null; 13 | }; 14 | -------------------------------------------------------------------------------- /tests/blackbox/setup/sequentialTests.d.ts: -------------------------------------------------------------------------------- 1 | export module 'sequentialTests'; 2 | export type SequentialTestEntry = { 3 | testFilePath: string; 4 | }; 5 | export const list: { 6 | before: SequentialTestEntry[]; 7 | after: SequentialTestEntry[]; 8 | only: SequentialTestEntry[]; 9 | }; 10 | export function getReversedTestIndex(testFilePath: string): number; 11 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210422A-remove-files-interface.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex('directus_fields').update({ interface: 'many-to-many' }).where({ interface: 'files' }); 5 | } 6 | 7 | export async function down(_knex: Knex): Promise { 8 | // Do nothing 9 | } 10 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-icon-file.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | " 5 |" 7 | `; 8 | -------------------------------------------------------------------------------- /app/src/components/v-card-actions.vue: -------------------------------------------------------------------------------- 1 | 2 |png 6 | 3 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /app/src/views/readme.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | Views are the top-level parent component that are used in all modules. Directus will only have two Views for the 4 | foreseeable future: `public` and `private` for non-authenticated and authenticated routes respectively. 5 | 6 | ## Table of Contents 7 | 8 | - [Private View](./private-view) 9 | - [Public View](./public) 10 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/display/typescript/source/display.vue: -------------------------------------------------------------------------------- 1 | 2 |Value: {{ value }}3 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /packages/utils/node/resolve-package.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { createRequire } from 'node:module'; 3 | 4 | const require = createRequire(import.meta.url); 5 | 6 | export function resolvePackage(name: string, root?: string): string { 7 | return path.dirname(require.resolve(`${name}/package.json`, root !== undefined ? { paths: [root] } : undefined)); 8 | } 9 | -------------------------------------------------------------------------------- /api/src/database/system-data/fields/migrations.yaml: -------------------------------------------------------------------------------- 1 | # directus_migrations isn't surfaced in the app, nor accessible from the API 2 | table: directus_migrations 3 | 4 | fields: 5 | - collection: directus_migrations 6 | field: version 7 | - collection: directus_migrations 8 | field: name 9 | - collection: directus_migrations 10 | field: timestamp 11 | -------------------------------------------------------------------------------- /api/src/database/system-data/fields/sessions.yaml: -------------------------------------------------------------------------------- 1 | table: directus_sessions 2 | 3 | fields: 4 | - field: token 5 | width: half 6 | - field: user 7 | width: half 8 | - field: expires 9 | width: half 10 | - field: ip 11 | width: half 12 | - field: user_agent 13 | width: half 14 | - field: origin 15 | width: half 16 | - field: share 17 | -------------------------------------------------------------------------------- /api/src/exceptions/hit-rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@directus/exceptions'; 2 | 3 | type Extensions = { 4 | limit: number; 5 | reset: Date; 6 | }; 7 | 8 | export class HitRateLimitException extends BaseException { 9 | constructor(message: string, extensions: Extensions) { 10 | super(message, 429, 'REQUESTS_EXCEEDED', extensions); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/middleware/schema.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from 'express'; 2 | import asyncHandler from '../utils/async-handler.js'; 3 | import { getSchema } from '../utils/get-schema.js'; 4 | 5 | const schema: RequestHandler = asyncHandler(async (req, _res, next) => { 6 | req.schema = await getSchema(); 7 | return next(); 8 | }); 9 | 10 | export default schema; 11 | -------------------------------------------------------------------------------- /api/src/utils/get-string-byte-size.test.ts: -------------------------------------------------------------------------------- 1 | import { stringByteSize } from '../../src/utils/get-string-byte-size.js'; 2 | import { test, expect } from 'vitest'; 3 | 4 | test('Returns correct byte size for given input string', () => { 5 | expect(stringByteSize('test')).toBe(4); 6 | expect(stringByteSize('🐡')).toBe(4); 7 | expect(stringByteSize('👨👧👦')).toBe(18); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | // Interfaces 2 | export * from './auth'; 3 | export * from './directus'; 4 | export * from './handlers'; 5 | export * from './items'; 6 | export * from './singleton'; 7 | export * from './storage'; 8 | export * from './transport'; 9 | 10 | // Implementations 11 | export * from './base'; 12 | 13 | // Types 14 | export * from './types'; 15 | -------------------------------------------------------------------------------- /packages/utils/node/readable-stream-to-string.ts: -------------------------------------------------------------------------------- 1 | import type { Readable } from 'node:stream'; 2 | 3 | export const readableStreamToString = async (stream: Readable): Promise=> { 4 | const chunks = []; 5 | 6 | for await (const chunk of stream) { 7 | chunks.push(Buffer.from(chunk)); 8 | } 9 | 10 | return Buffer.concat(chunks).toString('utf8'); 11 | }; 12 | -------------------------------------------------------------------------------- /api/src/operations/log/index.ts: -------------------------------------------------------------------------------- 1 | import { defineOperationApi, optionToString } from '@directus/utils'; 2 | import logger from '../../logger.js'; 3 | 4 | type Options = { 5 | message: unknown; 6 | }; 7 | 8 | export default defineOperationApi ({ 9 | id: 'log', 10 | 11 | handler: ({ message }) => { 12 | logger.info(optionToString(message)); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-skeleton-loader.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "" 8 | `; 9 | -------------------------------------------------------------------------------- /app/src/interfaces/presentation-links/preview.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/module/typescript/source/index.ts: -------------------------------------------------------------------------------- 1 | import { defineModule } from '@directus/extensions-sdk'; 2 | import ModuleComponent from './module.vue'; 3 | 4 | export default defineModule({ 5 | id: 'custom', 6 | name: 'Custom', 7 | icon: 'box', 8 | routes: [ 9 | { 10 | path: '', 11 | component: ModuleComponent, 12 | }, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /packages/specs/src/parameters/filter.yaml: -------------------------------------------------------------------------------- 1 | description: Select items in collection by given conditions. 2 | in: query 3 | name: filter 4 | required: false 5 | schema: 6 | type: array 7 | items: 8 | type: string 9 | pattern: '^(\[[^\[\]]*?\]){1}(\[(_eq|_neq|_lt|_lte|_gt|_gte|_in|_nin|_null|_nnull|_contains|_ncontains|_between|_nbetween|_empty|_nempty)\])?=.*?$' 10 | -------------------------------------------------------------------------------- /api/src/exceptions/method-not-allowed.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@directus/exceptions'; 2 | 3 | type Extensions = { 4 | allow: string[]; 5 | }; 6 | 7 | export class MethodNotAllowedException extends BaseException { 8 | constructor(message = 'Method not allowed.', extensions: Extensions) { 9 | super(message, 405, 'METHOD_NOT_ALLOWED', extensions); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/src/operations/sleep/index.ts: -------------------------------------------------------------------------------- 1 | import { defineOperationApi } from '@directus/utils'; 2 | 3 | type Options = { 4 | milliseconds: string | number; 5 | }; 6 | 7 | export default defineOperationApi ({ 8 | id: 'sleep', 9 | 10 | handler: async ({ milliseconds }) => { 11 | await new Promise((resolve) => setTimeout(resolve, Number(milliseconds))); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-overlay.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "" 8 | `; 9 | -------------------------------------------------------------------------------- /app/src/components/v-card-title.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /app/src/components/v-select/__snapshots__/v-select.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = `""`; 4 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-field/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemField from './system-field.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-field', 6 | name: '$t:field', 7 | icon: 'box', 8 | component: InterfaceSystemField, 9 | types: ['string'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/extensions-sdk/src/cli/utils/get-package-version.ts: -------------------------------------------------------------------------------- 1 | import { execa } from 'execa'; 2 | 3 | export default async function getPackageVersion(name: string, tag = 'latest'): Promise { 4 | const npmView = await execa('npm', ['view', name, '--json']); 5 | 6 | const packageInfo = JSON.parse(npmView.stdout); 7 | 8 | return packageInfo['dist-tags'][tag]; 9 | } 10 | -------------------------------------------------------------------------------- /api/src/database/helpers/date/dialects/mssql.ts: -------------------------------------------------------------------------------- 1 | import { DateHelper } from '../types.js'; 2 | import { parseISO } from 'date-fns'; 3 | 4 | export class DateHelperMSSQL extends DateHelper { 5 | override writeTimestamp(date: string): Date { 6 | const parsedDate = parseISO(date); 7 | return new Date(parsedDate.getTime() + parsedDate.getTimezoneOffset() * 60000); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/src/database/system-data/fields/notifications.yaml: -------------------------------------------------------------------------------- 1 | table: directus_notifications 2 | 3 | fields: 4 | - field: id 5 | - field: timestamp 6 | special: 7 | - date-created 8 | - cast-timestamp 9 | - field: status 10 | - field: recipient 11 | - field: sender 12 | - field: subject 13 | - field: message 14 | - field: collection 15 | - field: item 16 | -------------------------------------------------------------------------------- /app/src/interfaces/presentation-divider/preview.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/random/src/alpha.ts: -------------------------------------------------------------------------------- 1 | import { CHARACTERS_ALPHA } from './constants.js'; 2 | import { randomSequence } from './sequence.js'; 3 | 4 | /** 5 | * Return random string of alphabetic characters 6 | * 7 | * @param length - Length of the string to generate 8 | */ 9 | export const randomAlpha = (length: number) => { 10 | return randomSequence(length, CHARACTERS_ALPHA); 11 | }; 12 | -------------------------------------------------------------------------------- /tests/blackbox/utils/validate-date-difference.ts: -------------------------------------------------------------------------------- 1 | export function validateDateDifference(expectedDate: Date, receivedDate: Date, maxDifferenceMs: number): Date { 2 | const difference = Math.abs(expectedDate.getTime() - receivedDate.getTime()); 3 | 4 | // Return the received date if within the acceptable range 5 | return difference <= maxDifferenceMs ? receivedDate : expectedDate; 6 | } 7 | -------------------------------------------------------------------------------- /api/src/services/graphql/types/void.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | 3 | export const GraphQLVoid = new GraphQLScalarType({ 4 | name: 'Void', 5 | 6 | description: 'Represents NULL values', 7 | 8 | serialize() { 9 | return null; 10 | }, 11 | 12 | parseValue() { 13 | return null; 14 | }, 15 | 16 | parseLiteral() { 17 | return null; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /app/src/components/v-icon/custom-icons/grid_1.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /api/src/__mocks__/cache.mts: -------------------------------------------------------------------------------- 1 | import type { Mock } from 'vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | export const getCache: Mock = vi 5 | .fn() 6 | .mockReturnValue({ cache: undefined, systemCache: undefined, lockCache: undefined }); 7 | 8 | export const flushCaches: Mock = vi.fn(); 9 | export const clearSystemCache: Mock = vi.fn(); 10 | export const setSystemCache: Mock = vi.fn(); 11 | -------------------------------------------------------------------------------- /api/src/exceptions/service-unavailable.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@directus/exceptions'; 2 | 3 | type Extensions = { 4 | service: string; 5 | [key: string]: any; 6 | }; 7 | 8 | export class ServiceUnavailableException extends BaseException { 9 | constructor(message: string, extensions: Extensions) { 10 | super(message, 503, 'SERVICE_UNAVAILABLE', extensions); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/utils/strip-function.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { stripFunction } from './strip-function.js'; 3 | 4 | test.each([ 5 | { field: 'year(date_created)', expected: 'date_created' }, 6 | { field: 'test', expected: 'test' }, 7 | ])('should return "$expected" for "$field"', ({ field, expected }) => { 8 | expect(stripFunction(field)).toBe(expected); 9 | }); 10 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-highlight.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "Th 5 | is 6 | 7 | is 8 | a nice 9 | text" 10 | `; 11 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-mfa-setup/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemMFASetup from './system-mfa-setup.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-mfa-setup', 6 | name: 'mfa-setup', 7 | icon: 'box', 8 | component: InterfaceSystemMFASetup, 9 | types: ['text'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/types/activity.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@directus/types'; 2 | 3 | export type Activity = { 4 | id: number; 5 | action: 'comment'; 6 | user: null | Partial ; 7 | timestamp: string; 8 | edited_on: null | string; 9 | comment: null | string; 10 | }; 11 | 12 | export type ActivityByDate = { 13 | date: Date; 14 | dateFormatted: string; 15 | activity: Activity[]; 16 | }; 17 | -------------------------------------------------------------------------------- /api/src/types/graphql.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import type { DocumentNode } from 'graphql'; 3 | 4 | export interface GraphQLParams { 5 | query: string | null; 6 | variables: { readonly [name: string]: unknown } | null; 7 | operationName: string | null; 8 | document: DocumentNode; 9 | contextValue: { 10 | req?: Request; 11 | res?: Response; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-info.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | " 5 | 6 |" 9 | `; 10 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-field-tree/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemFieldTree from './system-field-tree.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-field-tree', 6 | name: '$t:field', 7 | icon: 'box', 8 | component: InterfaceSystemFieldTree, 9 | types: ['string'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-filter/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemFilter from './system-filter.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-filter', 6 | name: '$t:interfaces.filter.name', 7 | icon: 'search', 8 | component: InterfaceSystemFilter, 9 | types: ['json'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-scope/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemScope from './system-scope.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-scope', 6 | name: '$t:scope', 7 | icon: 'arrow_drop_down_circle', 8 | component: InterfaceSystemScope, 9 | types: ['string'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/utils/is-permission-empty.ts: -------------------------------------------------------------------------------- 1 | import type { Permission } from '@directus/types'; 2 | 3 | export function isPermissionEmpty(perm: Permission): boolean { 4 | return ( 5 | (perm.fields || []).length === 0 && 6 | Object.keys(perm.validation || {}).length === 0 && 7 | Object.keys(perm.presets || {}).length === 0 && 8 | Object.keys(perm.permissions || {}).length === 0 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/exceptions/src/base.ts: -------------------------------------------------------------------------------- 1 | export class BaseException extends Error { 2 | status: number; 3 | code: string; 4 | extensions: RecordThis is an info
7 |content
8 |; 5 | 6 | constructor(message: string, status: number, code: string, extensions?: Record ) { 7 | super(message); 8 | this.status = status; 9 | this.code = code; 10 | 11 | this.extensions = extensions || {}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/sdk/tests/base/directus.node.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment node 2 | import { Directus, MemoryStorage } from '../../src/base'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('node sdk', function () { 6 | const sdk = new Directus('http://example.com'); 7 | 8 | it('has storage', function () { 9 | expect(sdk.storage).toBeInstanceOf(MemoryStorage); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/specs/src/responses/notFoundError.yaml: -------------------------------------------------------------------------------- 1 | description: 'Error: Not found.' 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | properties: 7 | error: 8 | type: object 9 | properties: 10 | code: 11 | type: integer 12 | format: int64 13 | message: 14 | type: string 15 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-language/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemLanguage from './system-language.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-language', 6 | name: '$t:language', 7 | icon: 'translate', 8 | component: InterfaceSystemLanguage, 9 | system: true, 10 | types: ['string'], 11 | options: [], 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/types/panels.ts: -------------------------------------------------------------------------------- 1 | export type PanelFunction = 'count' | 'countDistinct' | 'avg' | 'avgDistinct' | 'sum' | 'sumDistinct' | 'min' | 'max'; 2 | 3 | export type BaseConditionalFillOperators = '=' | '!=' | '>' | '>=' | '<' | '<='; 4 | 5 | export type StringConditionalFillOperators = 6 | | BaseConditionalFillOperators 7 | | 'contains' 8 | | 'ncontains' 9 | | 'starts_with' 10 | | 'ends_with'; 11 | -------------------------------------------------------------------------------- /app/src/utils/get-special-for-type.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@directus/types'; 2 | 3 | export function getSpecialForType(type: Type): string[] | null { 4 | switch (type) { 5 | case 'json': 6 | case 'csv': 7 | case 'boolean': 8 | return ['cast-' + type]; 9 | case 'uuid': 10 | case 'hash': 11 | case 'geometry': 12 | return [type]; 13 | default: 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/sdk/src/storage.ts: -------------------------------------------------------------------------------- 1 | export abstract class IStorage { 2 | abstract auth_token: string | null; 3 | abstract auth_expires: number | null; 4 | abstract auth_expires_at: number | null; 5 | abstract auth_refresh_token: string | null; 6 | 7 | abstract get(key: string): string | null; 8 | abstract set(key: string, value: string): string; 9 | abstract delete(key: string): string | null; 10 | } 11 | -------------------------------------------------------------------------------- /tests/blackbox/setup/migrations/20221107_add_extensions_testing.js: -------------------------------------------------------------------------------- 1 | exports.up = async function (knex) { 2 | await knex.schema.createTable('tests_extensions_log', (table) => { 3 | table.increments('id').primary(); 4 | table.string('key'); 5 | table.string('value'); 6 | }); 7 | }; 8 | 9 | exports.down = async function (knex) { 10 | await knex.schema.dropTable('tests_extensions_log'); 11 | }; 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [directus] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /api/src/utils/strip-function.ts: -------------------------------------------------------------------------------- 1 | import { REGEX_BETWEEN_PARENS } from '@directus/constants'; 2 | 3 | /** 4 | * Strip the function declarations from a list of fields 5 | */ 6 | export function stripFunction(field: string): string { 7 | if (field.includes('(') && field.includes(')')) { 8 | return field.match(REGEX_BETWEEN_PARENS)?.[1]?.trim() ?? field; 9 | } else { 10 | return field; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-fields/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemFields from './system-fields.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-fields', 6 | name: '$t:interfaces.fields.name', 7 | icon: 'search', 8 | component: InterfaceSystemFields, 9 | types: ['csv', 'json'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-modules/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemModules from './system-modules.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-modules', 6 | name: '$t:module_bar', 7 | icon: 'arrow_drop_down_circle', 8 | component: InterfaceSystemModules, 9 | types: ['json'], 10 | options: [], 11 | system: true, 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/layouts/tabular/types.ts: -------------------------------------------------------------------------------- 1 | export type LayoutOptions = { 2 | widths?: { 3 | [field: string]: number; 4 | }; 5 | align?: { 6 | [field: string]: 'left' | 'center' | 'right'; 7 | }; 8 | limit?: number; 9 | spacing?: 'comfortable' | 'cozy' | 'compact'; 10 | }; 11 | 12 | export type LayoutQuery = { 13 | fields: string[]; 14 | sort: string[]; 15 | page: number; 16 | limit: number; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/specs/src/paths/server/ping.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | summary: Ping 3 | description: Ping, pong. Ping.. pong. 4 | operationId: ping 5 | responses: 6 | '200': 7 | content: 8 | application/text: 9 | schema: 10 | type: string 11 | pattern: 'pong' 12 | example: pong 13 | description: Successful request 14 | tags: 15 | - Server 16 | -------------------------------------------------------------------------------- /api/src/utils/get-date-formatted.ts: -------------------------------------------------------------------------------- 1 | export function getDateFormatted() { 2 | const date = new Date(); 3 | 4 | let month = String(date.getMonth() + 1); 5 | if (month.length === 1) month = '0' + month; 6 | 7 | let day = String(date.getDate()); 8 | if (day.length === 1) day = '0' + day; 9 | 10 | return `${date.getFullYear()}${month}${day}-${date.getHours()}${date.getMinutes()}${date.getSeconds()}`; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/components/v-sheet.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VSheet from './v-sheet.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VSheet).toBeTruthy(); 8 | 9 | const wrapper = mount(VSheet, { 10 | slots: { 11 | default: 'Slot Content', 12 | }, 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/extensions-sdk/src/cli/utils/get-sdk-version.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { dirname, resolve } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | const pkg = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json'), 'utf8')); 6 | 7 | export default function getSdkVersion(): string { 8 | return pkg.version; 9 | } 10 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/display/typescript/source/index.ts: -------------------------------------------------------------------------------- 1 | import { defineDisplay } from '@directus/extensions-sdk'; 2 | import DisplayComponent from './display.vue'; 3 | 4 | export default defineDisplay({ 5 | id: 'custom', 6 | name: 'Custom', 7 | icon: 'box', 8 | description: 'This is my custom display!', 9 | component: DisplayComponent, 10 | options: null, 11 | types: ['string'], 12 | }); 13 | -------------------------------------------------------------------------------- /packages/schema/src/types/table.ts: -------------------------------------------------------------------------------- 1 | export interface Table { 2 | name: string; 3 | 4 | // Not supported in SQLite + comment in mssql 5 | comment?: string | null; 6 | schema?: string; 7 | 8 | // MySQL Only 9 | collation?: string; 10 | engine?: string; 11 | 12 | // Postgres Only 13 | owner?: string; 14 | 15 | // SQLite Only 16 | sql?: string; 17 | 18 | //MSSQL only 19 | catalog?: string; 20 | } 21 | -------------------------------------------------------------------------------- /packages/specs/src/responses/unauthorizedError.yaml: -------------------------------------------------------------------------------- 1 | description: 'Error: Unauthorized request' 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | properties: 7 | error: 8 | type: object 9 | properties: 10 | code: 11 | type: integer 12 | format: int64 13 | message: 14 | type: string 15 | -------------------------------------------------------------------------------- /packages/types/src/shares.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './users.js'; 2 | 3 | export type Share = { 4 | id: string; 5 | name: string; 6 | collection: string; 7 | item: string; 8 | role: string; 9 | password: string; 10 | user_created: string | User; 11 | date_created: string; 12 | date_start: string | null; 13 | date_end: string | null; 14 | times_used: number; 15 | max_uses: number | null; 16 | }; 17 | -------------------------------------------------------------------------------- /api/src/utils/validate-env.ts: -------------------------------------------------------------------------------- 1 | import { getEnv } from '../env.js'; 2 | import logger from '../logger.js'; 3 | 4 | export function validateEnv(requiredKeys: string[]): void { 5 | const env = getEnv(); 6 | 7 | for (const requiredKey of requiredKeys) { 8 | if (requiredKey in env === false) { 9 | logger.error(`"${requiredKey}" Environment Variable is missing.`); 10 | process.exit(1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/displays/raw/index.ts: -------------------------------------------------------------------------------- 1 | import { defineDisplay } from '@directus/utils'; 2 | import { TYPES, LOCAL_TYPES } from '@directus/constants'; 3 | 4 | export default defineDisplay({ 5 | id: 'raw', 6 | name: '$t:displays.raw.raw', 7 | icon: 'code', 8 | component: ({ value }) => (typeof value === 'string' ? value : JSON.stringify(value)), 9 | options: [], 10 | types: TYPES, 11 | localTypes: LOCAL_TYPES, 12 | }); 13 | -------------------------------------------------------------------------------- /api/src/types/events.ts: -------------------------------------------------------------------------------- 1 | import type { ActionHandler, FilterHandler, InitHandler } from '@directus/types'; 2 | import type { ScheduledTask } from 'node-cron'; 3 | 4 | export type EventHandler = 5 | | { type: 'filter'; name: string; handler: FilterHandler } 6 | | { type: 'action'; name: string; handler: ActionHandler } 7 | | { type: 'init'; name: string; handler: InitHandler } 8 | | { type: 'schedule'; task: ScheduledTask }; 9 | -------------------------------------------------------------------------------- /app/src/utils/get-asset-url.ts: -------------------------------------------------------------------------------- 1 | import { addTokenToURL } from '@/api'; 2 | import { getPublicURL } from '@/utils/get-root-path'; 3 | 4 | export function getAssetUrl(filename: string, isDownload?: boolean): string { 5 | const assetUrl = new URL(`assets/${filename}`, getPublicURL()); 6 | 7 | if (isDownload) { 8 | assetUrl.searchParams.set('download', ''); 9 | } 10 | 11 | return addTokenToURL(assetUrl.href); 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/node/is-readable-stream.ts: -------------------------------------------------------------------------------- 1 | import type { Readable } from 'node:stream'; 2 | 3 | export const isReadableStream = (input: any): input is Readable => { 4 | return ( 5 | input !== null && 6 | typeof input === 'object' && 7 | typeof input.pipe === 'function' && 8 | typeof input._read === 'function' && 9 | typeof input._readableState === 'object' && 10 | input.readable !== false 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /api/src/exceptions/database/not-null-violation.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@directus/exceptions'; 2 | 3 | type Exceptions = { 4 | collection: string; 5 | field: string; 6 | }; 7 | 8 | export class NotNullViolationException extends BaseException { 9 | constructor(field: string, exceptions?: Exceptions) { 10 | super(`Value for field "${field}" can't be null.`, 400, 'NOT_NULL_VIOLATION', exceptions); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/src/utils/package.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { dirname, resolve } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)); 6 | 7 | const { name, version } = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')) as { 8 | name: string; 9 | version: string; 10 | }; 11 | 12 | export { name, version }; 13 | -------------------------------------------------------------------------------- /app/src/components/v-card-text.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VCardText from './v-card-text.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VCardText).toBeTruthy(); 8 | 9 | const wrapper = mount(VCardText, { 10 | slots: { 11 | default: 'Slot Content', 12 | }, 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /app/src/utils/hide-drag-image.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, vi } from 'vitest'; 2 | 3 | import { hideDragImage } from '@/utils/hide-drag-image'; 4 | 5 | test('Sets drag image to empty image', () => { 6 | const dataTransfer = { 7 | setDragImage: vi.fn() as any, 8 | } as DataTransfer; 9 | 10 | hideDragImage(dataTransfer); 11 | 12 | expect(dataTransfer.setDragImage).toHaveBeenCalledWith(new Image(), 0, 0); 13 | }); 14 | -------------------------------------------------------------------------------- /app/src/utils/sync-ref-property.ts: -------------------------------------------------------------------------------- 1 | import { computed, Ref, unref } from 'vue'; 2 | 3 | export function syncRefProperty (ref: Ref , key: T, defaultValue: R[T] | Ref ) { 4 | return computed ({ 5 | get() { 6 | return ref.value?.[key] ?? unref(defaultValue); 7 | }, 8 | set(value: R[T]) { 9 | ref.value = Object.assign({}, ref.value, { [key]: value }) as R; 10 | }, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /docs/app/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## HTTPS 4 | 5 | ## Password Salting/Hashing/Algorithm & Password Policy 6 | 7 | ## Static Tokens 8 | 9 | ## Encrypted Fields & Encrypted Database 10 | 11 | ## Private File Links 12 | 13 | ## MFA (Required) & SSO 14 | 15 | ## Granular Access Control & IP WHite-Listing & App Access 16 | 17 | ## Rate Limiting 18 | 19 | ## Encrypted 20 | 21 | ## Parameterized Database Queries 22 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/interface/typescript/source/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/extensions-sdk'; 2 | import InterfaceComponent from './interface.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'custom', 6 | name: 'Custom', 7 | icon: 'box', 8 | description: 'This is my custom interface!', 9 | component: InterfaceComponent, 10 | options: null, 11 | types: ['string'], 12 | }); 13 | -------------------------------------------------------------------------------- /packages/utils/node/pluralize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { depluralize, pluralize } from './pluralize.js'; 3 | 4 | describe('pluralize', () => { 5 | it('adds an s to the end of the string', () => { 6 | expect(pluralize('test')).toBe('tests'); 7 | }); 8 | 9 | it('removes an s to the end of the string', () => { 10 | expect(depluralize('tests')).toBe('test'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/utils/shared/pluralize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { depluralize, pluralize } from './pluralize.js'; 3 | 4 | describe('pluralize', () => { 5 | it('adds an s to the end of the string', () => { 6 | expect(pluralize('test')).toBe('tests'); 7 | }); 8 | 9 | it('removes an s to the end of the string', () => { 10 | expect(depluralize('tests')).toBe('test'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220614A-rename-hook-trigger-to-event.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex('directus_flows').update({ trigger: 'event' }).where('trigger', '=', 'hook'); 5 | } 6 | 7 | export async function down(knex: Knex): Promise { 8 | await knex('directus_flows').update({ trigger: 'hook' }).where('trigger', '=', 'event'); 9 | } 10 | -------------------------------------------------------------------------------- /api/src/exceptions/database/contains-null-values.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '@directus/exceptions'; 2 | 3 | type Exceptions = { 4 | collection: string; 5 | field: string; 6 | }; 7 | 8 | export class ContainsNullValuesException extends BaseException { 9 | constructor(field: string, exceptions?: Exceptions) { 10 | super(`Field "${field}" contains null values.`, 400, 'CONTAINS_NULL_VALUES', exceptions); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-radio.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "" 7 | `; 8 | -------------------------------------------------------------------------------- /app/src/components/v-card-title.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VCardTitle from './v-card-title.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VCardTitle).toBeTruthy(); 8 | 9 | const wrapper = mount(VCardTitle, { 10 | slots: { 11 | default: 'Slot Content', 12 | }, 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /app/src/components/v-icon/custom-icons/grid_2.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /packages/composables/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-collection.js'; 2 | export * from './use-custom-selection.js'; 3 | export * from './use-element-size.js'; 4 | export * from './use-filter-fields.js'; 5 | export * from './use-groupable.js'; 6 | export * from './use-items.js'; 7 | export * from './use-layout.js'; 8 | export * from './use-size-class.js'; 9 | export * from './use-sync.js'; 10 | export * from './use-system.js'; 11 | -------------------------------------------------------------------------------- /packages/schema/src/types/foreign-key.ts: -------------------------------------------------------------------------------- 1 | export type ForeignKey = { 2 | table: string; 3 | column: string; 4 | foreign_key_table: string; 5 | foreign_key_column: string; 6 | foreign_key_schema?: string; 7 | constraint_name: null | string; 8 | on_update: null | 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT'; 9 | on_delete: null | 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT'; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/utils/shared/get-fields-from-template.ts: -------------------------------------------------------------------------------- 1 | export function getFieldsFromTemplate(template: string | null): string[] { 2 | if (template === null) return []; 3 | 4 | const regex = /{{(.*?)}}/g; 5 | const fields = template.match(regex); 6 | 7 | if (!Array.isArray(fields)) { 8 | return []; 9 | } 10 | 11 | return fields.map((field) => { 12 | return field.replace(/{{/g, '').replace(/}}/g, '').trim(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/shared/get-simple-hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a simple short hash for a given string 3 | * This is not cryptographically secure in any way, and has a high chance of collision 4 | */ 5 | export function getSimpleHash(str: string) { 6 | let hash = 0; 7 | 8 | for (let i = 0; i < str.length; hash &= hash) { 9 | hash = 31 * hash + str.charCodeAt(i++); 10 | } 11 | 12 | return Math.abs(hash).toString(16); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/modules/settings/routes/flows/constants.ts: -------------------------------------------------------------------------------- 1 | import { Vector2 } from '@/utils/vector2'; 2 | 3 | const PANEL_WIDTH = 14; 4 | const PANEL_HEIGHT = 14; 5 | const ATTACHMENT_OFFSET = new Vector2(0, 3 * 20); 6 | const RESOLVE_OFFSET = new Vector2(PANEL_WIDTH * 20, 10 * 20); 7 | const REJECT_OFFSET = new Vector2(PANEL_WIDTH * 20, 12 * 20); 8 | 9 | export { PANEL_HEIGHT, PANEL_WIDTH, ATTACHMENT_OFFSET, RESOLVE_OFFSET, REJECT_OFFSET }; 10 | -------------------------------------------------------------------------------- /app/src/utils/localized-format.ts: -------------------------------------------------------------------------------- 1 | import { getDateFNSLocale } from '@/utils/get-date-fns-locale'; 2 | import formatOriginal from 'date-fns/format'; 3 | 4 | type LocalizedFormat = (...a: Parameters ) => string; 5 | 6 | export const localizedFormat: LocalizedFormat = (date, format, options): string => { 7 | return formatOriginal(date, format, { 8 | ...options, 9 | locale: getDateFNSLocale(), 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /app/src/components/v-card-actions.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VCardActions from './v-card-actions.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VCardActions).toBeTruthy(); 8 | 9 | const wrapper = mount(VCardActions, { 10 | slots: { 11 | default: 'Slot Content', 12 | }, 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /app/src/displays/file/index.ts: -------------------------------------------------------------------------------- 1 | import { defineDisplay } from '@directus/utils'; 2 | import DisplayFile from './file.vue'; 3 | 4 | export default defineDisplay({ 5 | id: 'file', 6 | name: '$t:displays.file.file', 7 | description: '$t:displays.file.description', 8 | icon: 'insert_drive_file', 9 | component: DisplayFile, 10 | types: ['uuid'], 11 | localTypes: ['file'], 12 | options: [], 13 | fields: ['id', 'type', 'title'], 14 | }); 15 | -------------------------------------------------------------------------------- /packages/schema/src/utils/extract-max-length.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the length value out of a given datatype 3 | * For example: `varchar(32)` => 32 4 | */ 5 | export default function extractMaxLength(type: string): null | number { 6 | const regex = /\(([^)]+)\)/; 7 | const matches = regex.exec(type); 8 | 9 | if (matches && matches.length > 0 && matches[1]) { 10 | return Number(matches[1]); 11 | } 12 | 13 | return null; 14 | } 15 | -------------------------------------------------------------------------------- /api/globalSetup.js: -------------------------------------------------------------------------------- 1 | // From https://sharp.pixelplumbing.com/install#worker-threads: 2 | // On some platforms, including glibc-based Linux, the main thread must call require('sharp') before worker threads are created. 3 | // This is to ensure shared libraries remain loaded in memory until after all threads are complete. 4 | // Without this, the following error may occur: Module did not self-register 5 | import 'sharp'; 6 | 7 | export default function () {} 8 | -------------------------------------------------------------------------------- /api/src/database/migrations/20211009A-add-auth-data.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_users', (table) => { 5 | table.json('auth_data'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_users', (table) => { 11 | table.dropColumn('auth_data'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/v-card-subtitle.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VCardSubtitle from './v-card-subtitle.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VCardSubtitle).toBeTruthy(); 8 | 9 | const wrapper = mount(VCardSubtitle, { 10 | slots: { 11 | default: 'Slot Content', 12 | }, 13 | }); 14 | 15 | expect(wrapper.html()).toMatchSnapshot(); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/random/src/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { randomUUID } from './uuid.js'; 3 | 4 | test('Returns random string', () => { 5 | expect(randomUUID()).toBeTypeOf('string'); 6 | }); 7 | 8 | test('Returns string in UUID format', () => { 9 | const uuid = randomUUID(); 10 | const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; 11 | 12 | expect(regex.test(uuid)).toBe(true); 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/slash-commands.yaml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v3 11 | with: 12 | token: ${{ secrets.LINEAR_PAT }} 13 | reactions: false 14 | commands: | 15 | linear 16 | -------------------------------------------------------------------------------- /api/src/utils/md.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | 3 | import { md } from './md.js'; 4 | 5 | test.each([ 6 | { str: 'test', expected: ' test
\n' }, 7 | { str: ``, expected: '' }, 8 | { str: `test`, expected: 'test
\n' }, 9 | ])('should sanitize "$str" into "$expected"', ({ str, expected }) => { 10 | expect(md(str)).toBe(expected); 11 | }); 12 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-divider.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "Default slot 5 |" 7 | `; 8 | -------------------------------------------------------------------------------- /app/src/views/register.ts: -------------------------------------------------------------------------------- 1 | import { App, defineAsyncComponent } from 'vue'; 2 | import PublicView from './public/'; 3 | import SharedView from './shared/shared-view.vue'; 4 | 5 | const PrivateView = defineAsyncComponent(() => import('./private')); 6 | 7 | export function registerViews(app: App): void { 8 | app.component('PublicView', PublicView); 9 | app.component('PrivateView', PrivateView); 10 | app.component('SharedView', SharedView); 11 | } 12 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/layout/javascript/source/layout.vue: -------------------------------------------------------------------------------- 1 | 2 |
6 |3 |6 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /api/src/database/migrations/20211016A-add-webhook-headers.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): PromiseName: {{ name }}
4 |Collection: {{ collection }}
5 |{ 4 | await knex.schema.alterTable('directus_webhooks', (table) => { 5 | table.json('headers'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_webhooks', (table) => { 11 | table.dropColumn('headers'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20211103A-set-unique-to-user-token.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_users', (table) => { 5 | table.unique(['token']); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_users', (table) => { 11 | table.dropUnique(['token']); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/layouts/map/types.ts: -------------------------------------------------------------------------------- 1 | import { CameraOptions } from 'maplibre-gl'; 2 | 3 | export type LayoutQuery = { 4 | fields: string[]; 5 | sort: string[]; 6 | limit: number; 7 | page: number; 8 | }; 9 | 10 | export type LayoutOptions = { 11 | cameraOptions?: CameraOptions & { bbox: any }; 12 | geometryField?: string; 13 | autoLocationFilter?: boolean; 14 | clusterData?: boolean; 15 | animateOptions?: any; 16 | displayTemplate?: string; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/utils/node/array-helpers.ts: -------------------------------------------------------------------------------- 1 | export function isIn (value: string, array: T): value is T[number] { 2 | return array.includes(value); 3 | } 4 | 5 | export function isTypeIn ( 6 | object: T, 7 | array: readonly E[] 8 | ): object is Extract { 9 | if (!object.type) return false; 10 | 11 | return (array as readonly string[]).includes(object.type); 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/shared/array-helpers.ts: -------------------------------------------------------------------------------- 1 | export function isIn (value: string, array: T): value is T[number] { 2 | return array.includes(value); 3 | } 4 | 5 | export function isTypeIn ( 6 | object: T, 7 | array: readonly E[] 8 | ): object is Extract { 9 | if (!object.type) return false; 10 | 11 | return (array as readonly string[]).includes(object.type); 12 | } 13 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210716A-add-conditions-to-fields.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_fields', (table) => { 5 | table.json('conditions'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_fields', (table) => { 11 | table.dropColumn('conditions'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/system-data/fields/dashboards.yaml: -------------------------------------------------------------------------------- 1 | table: directus_dashboards 2 | 3 | fields: 4 | - field: id 5 | special: 6 | - uuid 7 | - field: name 8 | - field: icon 9 | - field: panels 10 | special: 11 | - o2m 12 | - field: date_created 13 | special: 14 | - date-created 15 | - cast-timestamp 16 | - field: user_created 17 | special: 18 | - user-created 19 | - field: note 20 | - field: color 21 | -------------------------------------------------------------------------------- /api/src/utils/is-directus-jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | /** 4 | * Check if a given string conforms to the structure of a JWT 5 | * and whether it is issued by Directus. 6 | */ 7 | export default function isDirectusJWT(string: string): boolean { 8 | try { 9 | const payload = jwt.decode(string, { json: true }); 10 | if (payload?.iss !== 'directus') return false; 11 | return true; 12 | } catch { 13 | return false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/composables/src/use-sync.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import { computed } from 'vue'; 3 | 4 | export function useSync void>( 5 | props: T, 6 | key: K, 7 | emit: E 8 | ): Ref { 9 | return computed ({ 10 | get() { 11 | return props[key]; 12 | }, 13 | set(newVal) { 14 | emit(`update:${key}` as const, newVal); 15 | }, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/types/src/modules.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | import type { Permission } from './permissions.js'; 3 | import type { User } from './users.js'; 4 | 5 | export interface ModuleConfig { 6 | id: string; 7 | name: string; 8 | icon: string; 9 | color?: string; 10 | 11 | routes: RouteRecordRaw[]; 12 | hidden?: boolean; 13 | preRegisterCheck?: (user: User, permissions: Permission[]) => Promise | boolean; 14 | } 15 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220429C-drop-non-null-from-ip-of-activity.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_activity', (table) => { 5 | table.setNullable('ip'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_activity', (table) => { 11 | table.dropNullable('ip'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-inline-fields/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceInlineFields from './system-inline-fields.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-inline-fields', 6 | name: 'Inline Fields', 7 | description: 'Inline Fields', 8 | icon: 'box', 9 | component: InterfaceInlineFields, 10 | system: true, 11 | types: ['json'], 12 | group: 'standard', 13 | options: [], 14 | }); 15 | -------------------------------------------------------------------------------- /app/src/modules/settings/routes/data-model/field-detail/store/types.ts: -------------------------------------------------------------------------------- 1 | import { useFieldDetailStore } from './index'; 2 | import { DeepPartial } from '@directus/types'; 3 | 4 | export type StateUpdates = DeepPartial ['$state']>; 5 | export type State = ReturnType ['$state']; 6 | export type HelperFunctions = { 7 | getCurrent: (path: string) => any; 8 | hasChanged: (path: string) => boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /docs/app/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | > 4 | 5 | ## Is it possible to update the admin user password via CLI? 6 | 7 | You can do this with the following command: 8 | 9 | ```sh 10 | npx directus users passwd --email admin@example.com --password newpasswordhere 11 | ``` 12 | 13 | ## Why isn't Directus properly saving Chinese characters or emoji? 14 | 15 | Please ensure that the encoding for your database, tables, and fields are set to `utf8mb4`. 16 | -------------------------------------------------------------------------------- /packages/utils/shared/merge-filters.ts: -------------------------------------------------------------------------------- 1 | import type { Filter, LogicalFilterOR, LogicalFilterAND } from '@directus/types'; 2 | 3 | export function mergeFilters( 4 | filterA: Filter | null, 5 | filterB: Filter | null, 6 | strategy: 'and' | 'or' = 'and' 7 | ): Filter | null { 8 | if (!filterA) return filterB; 9 | if (!filterB) return filterA; 10 | 11 | return { 12 | [`_${strategy}`]: [filterA, filterB], 13 | } as LogicalFilterAND | LogicalFilterOR; 14 | } 15 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210831A-remove-limit-column.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_permissions', (table) => { 5 | table.dropColumn('limit'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_permissions', (table) => { 11 | table.integer('limit').unsigned(); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20211104A-remove-collections-listing.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_roles', (table) => { 5 | table.dropColumn('collection_list'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_roles', (table) => { 11 | table.json('collection_list'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/v-icon-file.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | 4 | import VIconFile from './v-icon-file.vue'; 5 | 6 | test('Mount component', () => { 7 | expect(VIconFile).toBeTruthy(); 8 | 9 | const wrapper = mount(VIconFile, { 10 | props: { 11 | ext: 'png', 12 | }, 13 | global: { 14 | stubs: ['v-icon'], 15 | }, 16 | }); 17 | 18 | expect(wrapper.html()).toMatchSnapshot(); 19 | }); 20 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-token/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemToken from './system-token.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-token', 6 | name: '$t:interfaces.system-token.system-token', 7 | description: '$t:interfaces.system-token.description', 8 | icon: 'vpn_key', 9 | component: InterfaceSystemToken, 10 | system: true, 11 | types: ['hash'], 12 | options: [], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/extensions-sdk/src/cli/utils/detect-json-indent.ts: -------------------------------------------------------------------------------- 1 | export default function detectJsonIndent(json: string) { 2 | const lines = json.split(/\r?\n/); 3 | 4 | const braceLine = lines.findIndex((line) => /^(?:\{|\[)/.test(line)); 5 | 6 | if (braceLine === -1 || braceLine + 1 > lines.length - 1) return null; 7 | 8 | const indent = lines[braceLine + 1]!.match(/[ \t]+/)?.[0]; 9 | 10 | if (indent === undefined) return null; 11 | 12 | return indent; 13 | } 14 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/layout/javascript/source/index.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import LayoutComponent from './layout.vue'; 3 | 4 | export default { 5 | id: 'custom', 6 | name: 'Custom', 7 | icon: 'box', 8 | component: LayoutComponent, 9 | slots: { 10 | options: () => null, 11 | sidebar: () => null, 12 | actions: () => null, 13 | }, 14 | setup() { 15 | const name = ref('Custom Layout'); 16 | 17 | return { name }; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/utils/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array-helpers.js'; 2 | export * from './ensure-extension-dirs.js'; 3 | export * from './generate-extensions-entrypoint.js'; 4 | export * from './get-extensions.js'; 5 | export * from './is-readable-stream.js'; 6 | export * from './list-folders.js'; 7 | export * from './path-to-relative-url.js'; 8 | export * from './pluralize.js'; 9 | export * from './readable-stream-to-string.js'; 10 | export * from './resolve-package.js'; 11 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210331A-add-refresh-interval.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_presets', (table) => { 5 | table.integer('refresh_interval'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_presets', (table) => { 11 | table.dropColumn('refresh_interval'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210521A-add-collections-icon-color.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_collections', (table) => { 5 | table.string('color').nullable(); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_collections', (table) => { 11 | table.dropColumn('color'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210803A-add-required-to-fields.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_fields', (table) => { 5 | table.boolean('required').defaultTo(false); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_fields', (table) => { 11 | table.dropColumn('required'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220429B-add-color-to-insights-icon.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_dashboards', (table) => { 5 | table.string('color').nullable(); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_dashboards', (table) => { 11 | table.dropColumn('color'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/interfaces/group-detail/preview.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /packages/sdk/src/handlers/roles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Roles handler 3 | */ 4 | 5 | import { ItemsHandler } from '../base/items'; 6 | import { ITransport } from '../transport'; 7 | import { RoleType, DefaultType } from '../types'; 8 | 9 | export type RoleItem = RoleType & T; 10 | 11 | export class RolesHandler extends ItemsHandler > { 12 | constructor(transport: ITransport) { 13 | super('directus_roles', transport); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220314A-add-translation-strings.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_settings', (table) => { 5 | table.json('translation_strings'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_settings', (table) => { 11 | table.dropColumn('translation_strings'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/utils/user-name.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@directus/types'; 2 | 3 | export function userName(user: Partial ): string { 4 | if (!user) { 5 | return 'Unknown User'; 6 | } 7 | 8 | if (user.first_name && user.last_name) { 9 | return `${user.first_name} ${user.last_name}`; 10 | } 11 | 12 | if (user.first_name) { 13 | return user.first_name; 14 | } 15 | 16 | if (user.email) { 17 | return user.email; 18 | } 19 | 20 | return 'Unknown User'; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/components/__snapshots__/v-button.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mount component 1`] = ` 4 | "" 9 | `; 10 | -------------------------------------------------------------------------------- /app/src/components/v-divider.stories.ts: -------------------------------------------------------------------------------- 1 | import VDivider from './v-divider.vue'; 2 | 3 | document.body.classList.add('light'); 4 | 5 | export default { 6 | title: 'Components/VDivider', 7 | component: VDivider, 8 | argTypes: {}, 9 | }; 10 | 11 | const Template = (args) => ({ 12 | setup() { 13 | return { args }; 14 | }, 15 | template: ' ', 16 | }); 17 | 18 | export const Primary = Template.bind({}); 19 | 20 | Primary.args = {}; 21 | -------------------------------------------------------------------------------- /packages/types/src/accountability.ts: -------------------------------------------------------------------------------- 1 | import type { Permission } from './permissions.js'; 2 | 3 | export type ShareScope = { 4 | collection: string; 5 | item: string; 6 | }; 7 | 8 | export type Accountability = { 9 | role: string | null; 10 | user?: string | null; 11 | admin?: boolean; 12 | app?: boolean; 13 | permissions?: Permission[]; 14 | share?: string; 15 | share_scope?: ShareScope; 16 | 17 | ip?: string; 18 | userAgent?: string; 19 | origin?: string; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/utils/shared/parse-json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run JSON.parse, but ignore `__proto__` properties. This prevents prototype pollution attacks 3 | */ 4 | export function parseJSON(input: string): any { 5 | if (String(input).includes('__proto__')) { 6 | return JSON.parse(input, noproto); 7 | } 8 | 9 | return JSON.parse(input); 10 | } 11 | 12 | export function noproto (key: string, value: T): T | void { 13 | if (key !== '__proto__') { 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210304A-remove-locked-fields.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_fields', (table) => { 5 | table.dropColumn('locked'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_fields', (table) => { 11 | table.boolean('locked').defaultTo(false).notNullable(); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220802A-add-custom-aspect-ratios.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_settings', (table) => { 5 | table.json('custom_aspect_ratios'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_settings', (table) => { 11 | table.dropColumn('custom_aspect_ratios'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/specs/src/paths/users/me-tfa-enable.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | summary: Enable 2FA 3 | description: Enables two-factor authentication for the currently authenticated user. 4 | operationId: meTfaEnable 5 | responses: 6 | '200': 7 | description: Successful request 8 | '401': 9 | $ref: '../../openapi.yaml#/components/responses/UnauthorizedError' 10 | '404': 11 | $ref: '../../openapi.yaml#/components/responses/NotFoundError' 12 | tags: 13 | - Users 14 | -------------------------------------------------------------------------------- /packages/utils/shared/to-array.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { toArray } from './to-array.js'; 3 | 4 | describe('toArray', () => { 5 | it('takes in a string and returns an array', () => { 6 | expect(toArray('1,2,3,4,5')).toStrictEqual(['1', '2', '3', '4', '5']); 7 | }); 8 | 9 | it('when passed an array returns the array', () => { 10 | expect(toArray(['1', '2', '3', '4', '5'])).toStrictEqual(['1', '2', '3', '4', '5']); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /api/src/utils/get-milliseconds.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | 3 | /** 4 | * Safely parse human readable time format into milliseconds 5 | */ 6 | export function getMilliseconds (value: unknown, fallback?: T): number | T; 7 | export function getMilliseconds(value: unknown, fallback = undefined): number | undefined { 8 | if ((typeof value !== 'string' && typeof value !== 'number') || value === '') { 9 | return fallback; 10 | } 11 | 12 | return ms(String(value)) ?? fallback; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-interface/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemInterface from './system-interface.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-interface', 6 | name: '$t:interfaces.system-interface.interface', 7 | description: '$t:interfaces.system-interface.description', 8 | icon: 'box', 9 | component: InterfaceSystemInterface, 10 | types: ['string'], 11 | system: true, 12 | options: [], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/operation/javascript/source/app.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'custom', 3 | name: 'Custom', 4 | icon: 'box', 5 | description: 'This is my custom operation!', 6 | overview: ({ text }) => [ 7 | { 8 | label: 'Text', 9 | text: text, 10 | }, 11 | ], 12 | options: [ 13 | { 14 | field: 'text', 15 | name: 'Text', 16 | type: 'string', 17 | meta: { 18 | width: 'full', 19 | interface: 'input', 20 | }, 21 | }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /packages/sdk/src/handlers/folders.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Folders handler 3 | */ 4 | 5 | import { ItemsHandler } from '../base/items'; 6 | import { ITransport } from '../transport'; 7 | import { FolderType, DefaultType } from '../types'; 8 | 9 | export type FolderItem = FolderType & T; 10 | 11 | export class FoldersHandler extends ItemsHandler > { 12 | constructor(transport: ITransport) { 13 | super('directus_folders', transport); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/sdk/src/handlers/presets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Presets handler 3 | */ 4 | 5 | import { ItemsHandler } from '../base/items'; 6 | import { ITransport } from '../transport'; 7 | import { PresetType, DefaultType } from '../types'; 8 | 9 | export type PresetItem = PresetType & T; 10 | 11 | export class PresetsHandler extends ItemsHandler > { 12 | constructor(transport: ITransport) { 13 | super('directus_presets', transport); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/specs/src/paths/users/me-tfa-disable.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | summary: Disable 2FA 3 | description: Disables two-factor authentication for the currently authenticated user. 4 | operationId: meTfaDisable 5 | responses: 6 | '200': 7 | description: Successful request 8 | '401': 9 | $ref: '../../openapi.yaml#/components/responses/UnauthorizedError' 10 | '404': 11 | $ref: '../../openapi.yaml#/components/responses/NotFoundError' 12 | tags: 13 | - Users 14 | -------------------------------------------------------------------------------- /packages/utils/shared/add-field-flag.ts: -------------------------------------------------------------------------------- 1 | import type { RawField, FieldMeta } from '@directus/types'; 2 | 3 | /** 4 | * Add a flag to a field. 5 | */ 6 | export function addFieldFlag(field: RawField, flag: string) { 7 | if (!field.meta) { 8 | field.meta = { 9 | special: [flag], 10 | } as FieldMeta; 11 | } else if (!field.meta.special) { 12 | field.meta.special = [flag]; 13 | } else if (!field.meta.special.includes(flag)) { 14 | field.meta.special.push(flag); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/blackbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "paths": { 11 | "@common/*": ["./common/*"], 12 | "@query/*": ["./query/*"], 13 | "@schema/*": ["./schema/*"], 14 | "@utils/*": ["./utils/*"] 15 | } 16 | }, 17 | "exclude": ["node_modules", "extensions"] 18 | } 19 | -------------------------------------------------------------------------------- /api/src/database/migrations/20211230A-add-project-descriptor.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_settings', (table) => { 5 | table.string('project_descriptor', 100).nullable(); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_settings', (table) => { 11 | table.dropColumn('project_descriptor'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/migrations/20220429D-drop-non-null-from-sender-of-notifications.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable('directus_notifications', (table) => { 5 | table.setNullable('sender'); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_notifications', (table) => { 11 | table.dropNullable('sender'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/database/seeds/12-sessions.yaml: -------------------------------------------------------------------------------- 1 | table: directus_sessions 2 | 3 | columns: 4 | token: 5 | type: string 6 | length: 64 7 | primary: true 8 | nullable: false 9 | user: 10 | type: uuid 11 | nullable: false 12 | references: 13 | table: directus_users 14 | column: id 15 | expires: 16 | type: timestamp 17 | nullable: false 18 | ip: 19 | type: string 20 | length: 255 21 | user_agent: 22 | type: string 23 | length: 255 24 | -------------------------------------------------------------------------------- /app/src/components/v-icon.stories.ts: -------------------------------------------------------------------------------- 1 | import VIcon from './v-icon/v-icon.vue'; 2 | 3 | document.body.classList.add('light'); 4 | 5 | export default { 6 | title: 'Components/VIcon', 7 | component: VIcon, 8 | argTypes: {}, 9 | }; 10 | 11 | const Template = (args) => ({ 12 | setup() { 13 | return { args }; 14 | }, 15 | template: ' ', 16 | }); 17 | 18 | export const Primary = Template.bind({}); 19 | 20 | Primary.args = { 21 | name: 'delete', 22 | }; 23 | -------------------------------------------------------------------------------- /app/src/interfaces/_system/system-folder/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/utils'; 2 | import InterfaceSystemFolder from './folder.vue'; 3 | 4 | export default defineInterface({ 5 | id: 'system-folder', 6 | name: '$t:interfaces.system-folder.folder', 7 | description: '$t:interfaces.system-folder.description', 8 | icon: 'folder', 9 | component: InterfaceSystemFolder, 10 | types: ['uuid'], 11 | options: [], 12 | system: true, 13 | recommendedDisplays: ['raw'], 14 | }); 15 | -------------------------------------------------------------------------------- /app/src/modules/settings/routes/project/components/project-info-sidebar-detail.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /app/src/types/notifications.ts: -------------------------------------------------------------------------------- 1 | export interface SnackbarRaw { 2 | id?: string; 3 | persist?: boolean; 4 | title: string; 5 | text?: string; 6 | type?: 'info' | 'success' | 'warning' | 'error'; 7 | code?: string; 8 | icon?: string | null; 9 | closeable?: boolean; 10 | progress?: number; 11 | loading?: boolean; 12 | dialog?: boolean; 13 | error?: Error; 14 | } 15 | 16 | export interface Snackbar extends SnackbarRaw { 17 | readonly id: string; 18 | readonly timestamp: number; 19 | } 20 | -------------------------------------------------------------------------------- /docs/guides/headless-cms/schedule-content/index.md: -------------------------------------------------------------------------------- 1 | # Scheduling Future Content 2 | 3 |8 | 9 | 14 | -------------------------------------------------------------------------------- /packages/random/src/integer.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { randomInteger } from './integer.js'; 3 | 4 | test('Returns random number in range', () => { 5 | const testInts = Array.from(Array(2)).map(() => Math.floor(Math.random() * 1000)); 6 | const min = Math.min(...testInts); 7 | const max = Math.max(...testInts); 8 | const output = randomInteger(min, max); 9 | expect(output).toBeGreaterThanOrEqual(min); 10 | expect(output).toBeLessThanOrEqual(max); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/types/src/permissions.ts: -------------------------------------------------------------------------------- 1 | import type { Filter } from './filter.js'; 2 | 3 | export type PermissionsAction = 'create' | 'read' | 'update' | 'delete' | 'comment' | 'explain' | 'share'; 4 | 5 | export type Permission = { 6 | id?: number; 7 | role: string | null; 8 | collection: string; 9 | action: PermissionsAction; 10 | permissions: Filter | null; 11 | validation: Filter | null; 12 | presets: Record | null; 13 | fields: string[] | null; 14 | system?: true; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/utils/shared/get-endpoint.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getEndpoint } from './get-endpoint.js'; 3 | 4 | describe('getEndpoint', () => { 5 | it('When a system collection is passed in', () => { 6 | expect(getEndpoint('directus_system_collection')).toBe('/system_collection'); 7 | }); 8 | 9 | it('When a non-system collection is passed in', () => { 10 | expect(getEndpoint('user_collection')).toBe('/items/user_collection'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/utils/shared/get-simple-hash.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getSimpleHash } from './get-simple-hash.js'; 3 | 4 | describe('getSimpleHash', () => { 5 | it('returns "364492" for string "test"', () => { 6 | expect(getSimpleHash('test')).toBe('364492'); 7 | }); 8 | 9 | it('returns "28cb67ba" for stringified object "{ key: \'value\' }"', () => { 10 | expect(getSimpleHash(JSON.stringify({ key: 'value' }))).toBe('28cb67ba'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210929A-rename-login-action.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex('directus_activity') 5 | .update({ 6 | action: 'login', 7 | }) 8 | .where('action', '=', 'authenticate'); 9 | } 10 | 11 | export async function down(knex: Knex): Promise { 12 | await knex('directus_activity') 13 | .update({ 14 | action: 'authenticate', 15 | }) 16 | .where('action', '=', 'login'); 17 | } 18 | -------------------------------------------------------------------------------- /api/src/types/express.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom properties on the req object in express 3 | */ 4 | 5 | import { Accountability, Query, SchemaOverview } from '@directus/types'; 6 | 7 | export {}; 8 | 9 | declare global { 10 | namespace Express { 11 | export interface Request { 12 | token: string | null; 13 | collection: string; 14 | sanitizedQuery: Query; 15 | schema: SchemaOverview; 16 | 17 | accountability?: Accountability; 18 | singleton?: boolean; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/lang/translations/si-LK.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | default_provider: පෙරනිමි 3 | draft: කටුපිටපත 4 | archived: සංරක්ෂිතය 5 | maps: සිතියම් 6 | group: සමූහය 7 | all: සියල්ල 8 | cancel: අවලංගු කරන්න 9 | interfaces: 10 | filter: 11 | all: සියල්ල 12 | displays: 13 | icon: 14 | icon: නිරූපකය 15 | layouts: 16 | cards: 17 | subtitle: උපසිරැසිය 18 | calendar: 19 | calendar: දින දසුන 20 | map: 21 | map: සිතියම 22 | layers: ස්ථර 23 | panels: 24 | metric: 25 | field: ක්ෂේත්රය 26 | -------------------------------------------------------------------------------- /app/src/routes/logout.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 |5 | 6 | 7 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /packages/random/src/sequence.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return string of given length comprised of characters of given character set 3 | * 4 | * @param length - Length of the string to generate 5 | * @param characters - Character set to use 6 | */ 7 | export const randomSequence = (length: number, characters: string) => { 8 | let result = ''; 9 | 10 | for (let i = length; i > 0; i--) { 11 | result += characters[Math.floor(Math.random() * characters.length)]; 12 | } 13 | 14 | return result; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/schema/src/types/overview.ts: -------------------------------------------------------------------------------- 1 | export type SchemaOverview = { 2 | [table: string]: { 3 | primary: string; 4 | columns: { 5 | [column: string]: { 6 | table_name: string; 7 | column_name: string; 8 | default_value: string | null; 9 | is_nullable: boolean; 10 | is_generated: boolean; 11 | data_type: string; 12 | numeric_precision?: number | null; 13 | numeric_scale?: number | null; 14 | max_length: number | null; 15 | }; 16 | }; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/sdk/tests/blog.d.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '../src/types'; 2 | 3 | export type Post = { 4 | id: ID; 5 | title: string; 6 | body: string; 7 | published: boolean; 8 | author: ID | Author; 9 | }; 10 | 11 | export type Category = { 12 | slug: string; 13 | name: string; 14 | }; 15 | 16 | export type Author = { 17 | id: ID; 18 | name: string; 19 | posts: (ID | Post)[]; 20 | }; 21 | 22 | export type Blog = { 23 | posts: Post; 24 | categories: Category; 25 | author: Author; 26 | }; 27 | -------------------------------------------------------------------------------- /api/src/database/migrations/20210608A-add-deep-clone-config.ts: -------------------------------------------------------------------------------- 1 | import type { Knex } from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise4 | { 4 | await knex.schema.alterTable('directus_collections', (table) => { 5 | table.json('item_duplication_fields').nullable(); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable('directus_collections', (table) => { 11 | table.dropColumn('item_duplication_fields'); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /api/src/middleware/use-collection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Set req.collection for use in other middleware. Used as an alternative on validate-collection for 3 | * system collections 4 | */ 5 | import type { RequestHandler } from 'express'; 6 | import asyncHandler from '../utils/async-handler.js'; 7 | 8 | const useCollection = (collection: string): RequestHandler => 9 | asyncHandler(async (req, _res, next) => { 10 | req.collection = collection; 11 | next(); 12 | }); 13 | 14 | export default useCollection; 15 | -------------------------------------------------------------------------------- /app/src/components/v-avatar.stories.ts: -------------------------------------------------------------------------------- 1 | import VAvatar from './v-avatar.vue'; 2 | 3 | document.body.classList.add('light'); 4 | 5 | export default { 6 | title: 'Components/VAvatar', 7 | component: VAvatar, 8 | argTypes: {}, 9 | }; 10 | 11 | const Template = (args) => ({ 12 | setup() { 13 | return { args }; 14 | }, 15 | template: ' ', 16 | }); 17 | 18 | export const Primary = Template.bind({}); 19 | 20 | Primary.args = {}; 21 | -------------------------------------------------------------------------------- /app/src/utils/localized-format-distance.ts: -------------------------------------------------------------------------------- 1 | import { getDateFNSLocale } from '@/utils/get-date-fns-locale'; 2 | import formatDistanceOriginal from 'date-fns/formatDistance'; 3 | 4 | type LocalizedFormatDistance = (...a: Parameters ) => string; 5 | 6 | export const localizedFormatDistance: LocalizedFormatDistance = (date, baseDate, options): string => { 7 | return formatDistanceOriginal(date, baseDate, { 8 | ...options, 9 | locale: getDateFNSLocale(), 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/interface/javascript/source/interface.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /packages/sdk/src/handlers/revisions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Revisions handler 3 | */ 4 | 5 | import { ItemsHandler } from '../base/items'; 6 | import { ITransport } from '../transport'; 7 | import { RevisionType, DefaultType } from '../types'; 8 | 9 | export type RevisionItem = RevisionType & T; 10 | 11 | export class RevisionsHandler extends ItemsHandler > { 12 | constructor(transport: ITransport) { 13 | super('directus_revisions', transport); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/node/readable-stream-to-string.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { Readable } from 'node:stream'; 3 | 4 | import { readableStreamToString } from './readable-stream-to-string.js'; 5 | 6 | test.each([Readable.from('test', { encoding: 'utf8' }), Readable.from(Buffer.from([0x74, 0x65, 0x73, 0x74]))])( 7 | 'Returns readable stream as string', 8 | async (readableStream) => { 9 | expect(readableStreamToString(readableStream)).resolves.toBe('test'); 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /packages/utils/shared/get-collection-type.ts: -------------------------------------------------------------------------------- 1 | import type { Collection, CollectionType } from '@directus/types'; 2 | 3 | /** 4 | * Get the type of collection. One of alias | table. (And later: view) 5 | * 6 | * @param collection Collection object to get the type of 7 | * @returns collection type 8 | */ 9 | export function getCollectionType(collection: Collection): CollectionType { 10 | if (collection.schema) return 'table'; 11 | if (collection.meta) return 'alias'; 12 | return 'unknown'; 13 | } 14 | -------------------------------------------------------------------------------- /packages/utils/shared/get-output-type-for-function.ts: -------------------------------------------------------------------------------- 1 | import type { FieldFunction, Type } from '@directus/types'; 2 | 3 | export function getOutputTypeForFunction(fn: FieldFunction): Type { 4 | const typeMap: Record = { 5 | year: 'integer', 6 | month: 'integer', 7 | week: 'integer', 8 | day: 'integer', 9 | weekday: 'integer', 10 | hour: 'integer', 11 | minute: 'integer', 12 | second: 'integer', 13 | count: 'integer', 14 | }; 15 | 16 | return typeMap[fn]; 17 | } 18 | -------------------------------------------------------------------------------- /packages/utils/shared/is-valid-json.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { isValidJSON } from './is-valid-json.js'; 3 | 4 | describe('isValidJSON', () => { 5 | it('returns true if JSON is valid', () => { 6 | const result = isValidJSON(`{"name": "Directus"}`); 7 | expect(result).toEqual(true); 8 | }); 9 | 10 | it('returns false if JSON is invalid', () => { 11 | const result = isValidJSON(`{"name: Directus"}`); 12 | expect(result).toEqual(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /api/src/database/system-data/fields/revisions.yaml: -------------------------------------------------------------------------------- 1 | table: directus_revisions 2 | 3 | fields: 4 | - field: id 5 | width: half 6 | 7 | - field: activity 8 | width: half 9 | 10 | - field: collection 11 | width: half 12 | 13 | - field: item 14 | width: half 15 | 16 | - field: data 17 | hidden: true 18 | special: 19 | - cast-json 20 | 21 | - field: delta 22 | hidden: true 23 | special: 24 | - cast-json 25 | 26 | - field: parent 27 | width: half 28 | -------------------------------------------------------------------------------- /api/src/utils/get-collection-from-alias.ts: -------------------------------------------------------------------------------- 1 | import type { AliasMap } from './get-column-path.js'; 2 | 3 | /** 4 | * Extract the collection of an alias within an aliasMap 5 | * For example: 'ljnsv.name' -> 'authors' 6 | */ 7 | export function getCollectionFromAlias(alias: string, aliasMap: AliasMap): string | undefined { 8 | for (const aliasValue of Object.values(aliasMap)) { 9 | if (aliasValue.alias === alias) { 10 | return aliasValue.collection; 11 | } 12 | } 13 | 14 | return undefined; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/components/v-icon/custom-icons/flip_horizontal.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /app/src/utils/extract-field-from-function.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | import { extractFieldFromFunction } from '@/utils/extract-field-from-function'; 4 | 5 | test('Returns original field if no function is given', () => { 6 | expect(extractFieldFromFunction('title')).toEqual({ fn: null, field: 'title' }); 7 | }); 8 | 9 | test('Returns function extracted', () => { 10 | expect(extractFieldFromFunction('year(date_created)')).toEqual({ fn: 'year', field: 'date_created' }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/extensions-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useApi, 3 | useCollection, 4 | useExtensions, 5 | useFilterFields, 6 | useItems, 7 | useLayout, 8 | useStores, 9 | useSync, 10 | } from '@directus/composables'; 11 | export { 12 | defineDisplay, 13 | defineEndpoint, 14 | defineHook, 15 | defineInterface, 16 | defineLayout, 17 | defineModule, 18 | defineOperationApi, 19 | defineOperationApp, 20 | definePanel, 21 | getFieldsFromTemplate, 22 | getRelationType, 23 | } from '@directus/utils'; 24 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/panel/javascript/source/index.js: -------------------------------------------------------------------------------- 1 | import PanelComponent from './panel.vue'; 2 | 3 | export default { 4 | id: 'custom', 5 | name: 'Custom', 6 | icon: 'box', 7 | description: 'This is my custom panel!', 8 | component: PanelComponent, 9 | options: [ 10 | { 11 | field: 'text', 12 | name: 'Text', 13 | type: 'string', 14 | meta: { 15 | interface: 'input', 16 | width: 'full', 17 | }, 18 | }, 19 | ], 20 | minWidth: 12, 21 | minHeight: 8, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/specs/src/parameters/sort.yaml: -------------------------------------------------------------------------------- 1 | description: > 2 | How to sort the returned items. `sort` is a CSV of fields used to sort the fetched items. Sorting defaults to 3 | ascending (ASC) order but a minus sign (` - `) can be used to reverse this to descending (DESC) order. Fields are 4 | prioritized by their order in the CSV. You can also use a ` ? ` to sort randomly. 5 | in: query 6 | name: sort 7 | required: false 8 | explode: false 9 | schema: 10 | type: array 11 | items: 12 | type: string 13 | -------------------------------------------------------------------------------- /app/src/components/v-chip.stories.ts: -------------------------------------------------------------------------------- 1 | import VChip from './v-chip.vue'; 2 | 3 | document.body.classList.add('light'); 4 | 5 | export default { 6 | title: 'Components/VChip', 7 | component: VChip, 8 | argTypes: { 9 | close: { control: 'boolean' }, 10 | }, 11 | }; 12 | 13 | const Template = (args) => ({ 14 | setup() { 15 | return { args }; 16 | }, 17 | template: ' Cake ', 18 | }); 19 | 20 | export const Primary = Template.bind({}); 21 | 22 | Primary.args = {}; 23 | -------------------------------------------------------------------------------- /app/src/components/v-icon/custom-icons/grid_3.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /app/src/modules/settings/routes/data-model/field-detail/store/alterations/index.ts: -------------------------------------------------------------------------------- 1 | export * as global from './global'; 2 | export * as file from './file'; 3 | export * as files from './files'; 4 | export * as group from './group'; 5 | export * as m2a from './m2a'; 6 | export * as m2m from './m2m'; 7 | export * as m2o from './m2o'; 8 | export * as o2m from './o2m'; 9 | export * as presentation from './presentation'; 10 | export * as standard from './standard'; 11 | export * as translations from './translations'; 12 | -------------------------------------------------------------------------------- /packages/schema/src/utils/strip-quotes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Strip leading/trailing quotes from a string and handle null values. 3 | */ 4 | export function stripQuotes(value?: string | null): string | null { 5 | if (value === null || value === undefined) { 6 | return null; 7 | } 8 | 9 | const trimmed = value.trim(); 10 | 11 | if ((trimmed.startsWith(`'`) && trimmed.endsWith(`'`)) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) { 12 | return trimmed.slice(1, -1); 13 | } 14 | 15 | return value; 16 | } 17 | -------------------------------------------------------------------------------- /packages/utils/shared/get-fields-from-template.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getFieldsFromTemplate } from './get-fields-from-template.js'; 3 | 4 | describe('getFieldsFromTemplate', () => { 5 | it('returns an empty array when passed null', () => { 6 | expect(getFieldsFromTemplate(null)).toStrictEqual([]); 7 | }); 8 | 9 | it('returns fields as an array of strings', () => { 10 | expect(getFieldsFromTemplate('{{ field }}')).toStrictEqual(['field']); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /api/src/database/helpers/fn/index.ts: -------------------------------------------------------------------------------- 1 | export { FnHelperPostgres as postgres } from './dialects/postgres.js'; 2 | export { FnHelperPostgres as redshift } from './dialects/postgres.js'; 3 | export { FnHelperPostgres as cockroachdb } from './dialects/postgres.js'; 4 | export { FnHelperOracle as oracle } from './dialects/oracle.js'; 5 | export { FnHelperSQLite as sqlite } from './dialects/sqlite.js'; 6 | export { FnHelperMySQL as mysql } from './dialects/mysql.js'; 7 | export { FnHelperMSSQL as mssql } from './dialects/mssql.js'; 8 | -------------------------------------------------------------------------------- /docs/public/icons/card_link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/types/src/presets.ts: -------------------------------------------------------------------------------- 1 | import type { Filter } from './filter.js'; 2 | 3 | export type Preset = { 4 | id?: number; 5 | bookmark: string | null; 6 | icon: string; 7 | color?: string | null; 8 | user: string | null; 9 | role: string | null; 10 | collection: string; 11 | search: string | null; 12 | filter: Filter | null; 13 | layout: string | null; 14 | layout_query: { [layout: string]: any } | null; 15 | layout_options: { [layout: string]: any } | null; 16 | refresh_interval: number | null; 17 | }; 18 | -------------------------------------------------------------------------------- /api/src/cli/commands/database/install.ts: -------------------------------------------------------------------------------- 1 | import installSeeds from '../../../database/seeds/run.js'; 2 | import getDatabase from '../../../database/index.js'; 3 | import logger from '../../../logger.js'; 4 | 5 | export default async function start(): Promise{ 6 | const database = getDatabase(); 7 | 8 | try { 9 | await installSeeds(database); 10 | database.destroy(); 11 | process.exit(0); 12 | } catch (err: any) { 13 | logger.error(err); 14 | database.destroy(); 15 | process.exit(1); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/src/services/mail/templates/user-invitation.liquid: -------------------------------------------------------------------------------- 1 | {% layout "base" %} 2 | {% block content %} 3 | 4 | 5 | You have been invited to join {{ projectName }}. Please click the button below to accept this invitation and join the project: 6 |
7 | 8 |9 | 10 | Join {{ projectName }} 11 | 12 |
13 | 14 |15 | Thank you,
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /api/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './assets.js'; 2 | export * from './ast.js'; 3 | export * from './auth.js'; 4 | export * from './collection.js'; 5 | export * from './database.js'; 6 | export * from './events.js'; 7 | export * from './files.js'; 8 | export * from './graphql.js'; 9 | export * from './items.js'; 10 | export * from './meta.js'; 11 | export * from './migration.js'; 12 | export * from './revision.js'; 13 | export * from './services.js'; 14 | export * from './snapshot.js'; 15 | export * from './webhooks.js'; 16 | -------------------------------------------------------------------------------- /app/src/interfaces/input-autocomplete-api/preview.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /packages/extensions-sdk/templates/layout/typescript/source/layout.vue: -------------------------------------------------------------------------------- 1 | 2 |
16 | The {{ projectName }} Team 17 |3 |6 | 7 | 8 | 25 | --------------------------------------------------------------------------------Name: {{ name }}
4 |Collection: {{ collection }}
5 |