From dae832c28840423dbc3cb01223c72506d8afb6f1 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:47:18 +0200 Subject: [PATCH] feat: select fields (#8550) Adds `select` which is used to specify the field projection for local and rest API calls. This is available as an optimization to reduce the payload's of requests and make the database queries more efficient. Includes: - [x] generate types for the `select` property - [x] infer the return type by `select` with 2 modes - include (`field: true`) and exclude (`field: false`) - [x] lots of integration tests, including deep fields / localization etc - [x] implement the property in db adapters - [x] implement the property in the local api for most operations - [x] implement the property in the rest api - [x] docs --------- Co-authored-by: Dan Ribbens --- docs/local-api/overview.mdx | 31 +- docs/queries/select.mdx | 99 + docs/rest-api/overview.mdx | 1 + packages/db-mongodb/src/deleteOne.ts | 12 +- packages/db-mongodb/src/find.ts | 10 + packages/db-mongodb/src/findGlobal.ts | 8 +- packages/db-mongodb/src/findGlobalVersions.ts | 3 + packages/db-mongodb/src/findOne.ts | 13 +- packages/db-mongodb/src/findVersions.ts | 9 +- packages/db-mongodb/src/queryDrafts.ts | 11 +- packages/db-mongodb/src/updateGlobal.ts | 8 +- .../db-mongodb/src/updateGlobalVersion.ts | 13 +- packages/db-mongodb/src/updateOne.ts | 6 +- packages/db-mongodb/src/updateVersion.ts | 14 +- packages/db-mongodb/src/upsert.ts | 4 +- .../src/utilities/buildJoinAggregation.ts | 10 + .../utilities/buildProjectionFromSelect.ts | 234 +++ packages/drizzle/src/create.ts | 3 +- packages/drizzle/src/createGlobalVersion.ts | 2 + packages/drizzle/src/createVersion.ts | 2 + packages/drizzle/src/deleteOne.ts | 9 +- packages/drizzle/src/find.ts | 2 + .../drizzle/src/find/buildFindManyArgs.ts | 102 +- packages/drizzle/src/find/findMany.ts | 2 + packages/drizzle/src/find/traverseFields.ts | 237 ++- packages/drizzle/src/findGlobal.ts | 3 +- packages/drizzle/src/findGlobalVersions.ts | 2 + packages/drizzle/src/findOne.ts | 3 +- packages/drizzle/src/findVersions.ts | 2 + packages/drizzle/src/queryDrafts.ts | 4 +- packages/drizzle/src/update.ts | 3 +- packages/drizzle/src/updateGlobal.ts | 3 +- packages/drizzle/src/updateGlobalVersion.ts | 2 + packages/drizzle/src/updateVersion.ts | 2 + packages/drizzle/src/upsertRow/index.ts | 2 + packages/drizzle/src/upsertRow/types.ts | 4 +- .../graphql/src/resolvers/globals/update.ts | 10 +- .../src/routes/rest/collections/create.ts | 2 + .../src/routes/rest/collections/delete.ts | 5 +- .../src/routes/rest/collections/deleteByID.ts | 2 + .../src/routes/rest/collections/duplicate.ts | 2 + .../next/src/routes/rest/collections/find.ts | 5 +- .../src/routes/rest/collections/findByID.ts | 2 + .../rest/collections/findVersionByID.ts | 2 + .../routes/rest/collections/findVersions.ts | 5 +- .../src/routes/rest/collections/update.ts | 5 +- .../src/routes/rest/collections/updateByID.ts | 2 + .../next/src/routes/rest/globals/findOne.ts | 2 + .../routes/rest/globals/findVersionByID.ts | 2 + .../src/routes/rest/globals/findVersions.ts | 5 +- .../next/src/routes/rest/globals/update.ts | 2 + .../routes/rest/utilities/sanitizeSelect.ts | 20 + .../src/auth/operations/registerFirstUser.ts | 4 +- .../src/auth/strategies/local/register.ts | 5 +- packages/payload/src/bin/generateTypes.ts | 3 + .../payload/src/collections/config/types.ts | 16 +- .../src/collections/operations/create.ts | 21 +- .../src/collections/operations/delete.ts | 27 +- .../src/collections/operations/deleteByID.ts | 16 +- .../src/collections/operations/duplicate.ts | 28 +- .../src/collections/operations/find.ts | 29 +- .../src/collections/operations/findByID.ts | 31 +- .../collections/operations/findVersionByID.ts | 6 +- .../collections/operations/findVersions.ts | 6 +- .../collections/operations/local/create.ts | 29 +- .../collections/operations/local/delete.ts | 75 +- .../collections/operations/local/duplicate.ts | 26 +- .../src/collections/operations/local/find.ts | 29 +- .../collections/operations/local/findByID.ts | 40 +- .../operations/local/findVersionByID.ts | 5 +- .../operations/local/findVersions.ts | 12 +- .../src/collections/operations/local/index.ts | 1 + .../operations/local/restoreVersion.ts | 13 +- .../collections/operations/local/update.ts | 75 +- .../collections/operations/restoreVersion.ts | 6 +- .../src/collections/operations/update.ts | 15 +- .../src/collections/operations/updateByID.ts | 21 +- .../src/collections/operations/utils.ts | 30 +- packages/payload/src/database/types.ts | 24 +- packages/payload/src/exports/shared.ts | 4 +- .../src/fields/hooks/afterRead/index.ts | 12 +- .../src/fields/hooks/afterRead/promise.ts | 81 +- .../fields/hooks/afterRead/traverseFields.ts | 14 +- packages/payload/src/globals/config/types.ts | 4 +- .../payload/src/globals/operations/findOne.ts | 21 +- .../src/globals/operations/findVersionByID.ts | 7 +- .../src/globals/operations/findVersions.ts | 50 +- .../src/globals/operations/local/findOne.ts | 23 +- .../operations/local/findVersionByID.ts | 5 +- .../globals/operations/local/findVersions.ts | 6 +- .../operations/local/restoreVersion.ts | 1 + .../src/globals/operations/local/update.ts | 25 +- .../payload/src/globals/operations/update.ts | 42 +- packages/payload/src/index.ts | 136 +- packages/payload/src/types/index.ts | 84 +- .../addSelectGenericsToGeneratedTyoes.spec.ts | 564 ++++++ .../addSelectGenericsToGeneretedTypes.ts | 51 + .../src/utilities/configToJSONSchema.ts | 153 +- .../payload/src/utilities/getSelectMode.ts | 16 + .../versions/drafts/getQueryDraftsSelect.ts | 26 + .../drafts/replaceWithDraftIfAvailable.ts | 21 +- packages/payload/src/versions/saveVersion.ts | 9 +- test/helpers/NextRESTClient.ts | 35 +- test/joins/int.spec.ts | 39 + test/joins/payload-types.ts | 157 ++ test/select/.gitignore | 2 + test/select/collections/DeepPosts/index.ts | 76 + .../collections/LocalizedPosts/index.ts | 147 ++ test/select/collections/Posts/index.ts | 78 + .../collections/VersionedPosts/index.ts | 43 + test/select/config.ts | 73 + test/select/eslint.config.js | 19 + test/select/int.spec.ts | 1677 +++++++++++++++++ test/select/payload-types.ts | 604 ++++++ test/select/tsconfig.eslint.json | 13 + test/select/tsconfig.json | 3 + 116 files changed, 5491 insertions(+), 371 deletions(-) create mode 100644 docs/queries/select.mdx create mode 100644 packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts create mode 100644 packages/next/src/routes/rest/utilities/sanitizeSelect.ts create mode 100644 packages/payload/src/utilities/addSelectGenericsToGeneratedTyoes.spec.ts create mode 100644 packages/payload/src/utilities/addSelectGenericsToGeneretedTypes.ts create mode 100644 packages/payload/src/utilities/getSelectMode.ts create mode 100644 packages/payload/src/versions/drafts/getQueryDraftsSelect.ts create mode 100644 test/select/.gitignore create mode 100644 test/select/collections/DeepPosts/index.ts create mode 100644 test/select/collections/LocalizedPosts/index.ts create mode 100644 test/select/collections/Posts/index.ts create mode 100644 test/select/collections/VersionedPosts/index.ts create mode 100644 test/select/config.ts create mode 100644 test/select/eslint.config.js create mode 100644 test/select/int.spec.ts create mode 100644 test/select/payload-types.ts create mode 100644 test/select/tsconfig.eslint.json create mode 100644 test/select/tsconfig.json diff --git a/docs/local-api/overview.mdx b/docs/local-api/overview.mdx index eca5e20a07..666be3dcee 100644 --- a/docs/local-api/overview.mdx +++ b/docs/local-api/overview.mdx @@ -77,21 +77,22 @@ Both options function in exactly the same way outside of one having HMR support You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in. -| Local Option | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | -| `data` | The data to use within the operation. Required for `create`, `update`. | -| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | -| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | -| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | -| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | -| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). | -| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | -| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | -| `pagination` | Set to false to return all documents and avoid querying for document counts. | -| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | -| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | -| `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. | +| Local Option | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | +| `data` | The data to use within the operation. Required for `create`, `update`. | +| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | +| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | +| `select` | Specify [select](../queries/select) to control which fields to include to the result. | +| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | +| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | +| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). | +| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | +| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | +| `pagination` | Set to false to return all documents and avoid querying for document counts. | +| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | +| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | +| `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. | _There are more options available on an operation by operation basis outlined below._ diff --git a/docs/queries/select.mdx b/docs/queries/select.mdx new file mode 100644 index 0000000000..3bd4fe694a --- /dev/null +++ b/docs/queries/select.mdx @@ -0,0 +1,99 @@ +--- +title: Select +label: Select +order: 30 +desc: Payload select determines which fields are selected to the result. +keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs +--- + +You may not need the full data from your Local API / REST queries, but only some specific fields. The select fields API can help you to optimize those cases. + +## Local API + +To specify select in the [Local API](../local-api/overview), you can use the `select` option in your query: + +```ts +// Include mode +const getPosts = async () => { + const posts = await payload.find({ + collection: 'posts', + select: { + text: true, + // select a specific field from group + group: { + number: true + }, + // select all fields from array + array: true, + }, // highlight-line + }) + + return posts +} + +// Exclude mode +const getPosts = async () => { + const posts = await payload.find({ + collection: 'posts', + // Select everything except for array and group.number + select: { + array: false, + group: { + number: false + } + }, // highlight-line + }) + + return posts +} +``` + + + + Important: + To perform querying with `select` efficiently, it works on the database level. Because of that, your `beforeRead` and `afterRead` hooks may not receive the full `doc`. + + + +## REST API + +To specify select in the [REST API](../rest-api/overview), you can use the `select` parameter in your query: + +```ts +fetch('https://localhost:3000/api/posts?select[color]=true&select[group][number]=true') // highlight-line + .then((res) => res.json()) + .then((data) => console.log(data)) +``` + +To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write. + +For this reason, we recommend to use the extremely helpful and ubiquitous [`qs`](https://www.npmjs.com/package/qs) package to parse your JSON / object-formatted queries into query strings: + +```ts +import { stringify } from 'qs-esm' + +const select = { + text: true, + group: { + number: true + } + // This query could be much more complex + // and QS would handle it beautifully +} + +const getPosts = async () => { + const stringifiedQuery = stringify( + { + select, // ensure that `qs` adds the `select` property, too! + }, + { addQueryPrefix: true }, + ) + + const response = await fetch(`http://localhost:3000/api/posts${stringifiedQuery}`) + // Continue to handle the response below... +} + + + Reminder: + This is the same for [Globals](../configuration/globals) using the `/api/globals` endpoint. + diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index a41f40deab..2f3003f0e6 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -18,6 +18,7 @@ All Payload API routes are mounted and prefixed to your config's `routes.api` UR - [depth](../queries/depth) - automatically populates relationships and uploads - [locale](/docs/configuration/localization#retrieving-localized-docs) - retrieves document(s) in a specific locale - [fallback-locale](/docs/configuration/localization#retrieving-localized-docs) - specifies a fallback locale if no locale value exists +- [select](../queries/select) - speicifes which fields to include to the result ## Collections diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 5cf4895b49..889b4a8e75 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -2,12 +2,13 @@ import type { DeleteOne, Document, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' export const deleteOne: DeleteOne = async function deleteOne( this: MongooseAdapter, - { collection, req = {} as PayloadRequest, where }, + { collection, req = {} as PayloadRequest, select, where }, ) { const Model = this.collections[collection] const options = await withSession(this, req) @@ -17,7 +18,14 @@ export const deleteOne: DeleteOne = async function deleteOne( where, }) - const doc = await Model.findOneAndDelete(query, options).lean() + const doc = await Model.findOneAndDelete(query, { + ...options, + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.collections[collection].config.fields, + select, + }), + }).lean() let result: Document = JSON.parse(JSON.stringify(doc)) diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index b88f66c784..16529d25c4 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -7,6 +7,7 @@ import type { MongooseAdapter } from './index.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' @@ -21,6 +22,7 @@ export const find: Find = async function find( pagination, projection, req = {} as PayloadRequest, + select, sort: sortArg, where, }, @@ -67,6 +69,14 @@ export const find: Find = async function find( useEstimatedCount, } + if (select) { + paginationOptions.projection = buildProjectionFromSelect({ + adapter: this, + fields: collectionConfig.fields, + select, + }) + } + if (this.collation) { const defaultLocale = 'en' paginationOptions.collation = { diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 0473674dc4..a32c8ca729 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -4,17 +4,23 @@ import { combineQueries } from 'payload' import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' export const findGlobal: FindGlobal = async function findGlobal( this: MongooseAdapter, - { slug, locale, req = {} as PayloadRequest, where }, + { slug, locale, req = {} as PayloadRequest, select, where }, ) { const Model = this.globals const options = { ...(await withSession(this, req)), lean: true, + select: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.globals.config.find((each) => each.slug === slug).fields, + select, + }), } const query = await Model.buildQuery({ diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 61c6cbcf2f..5a82355ef1 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -6,6 +6,7 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' import { buildSortParam } from './queries/buildSortParam.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' @@ -18,6 +19,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV page, pagination, req = {} as PayloadRequest, + select, skip, sort: sortArg, where, @@ -69,6 +71,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV options, page, pagination, + projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }), sort, useEstimatedCount, } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index d2be516567..47f5615b90 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,15 +1,16 @@ -import type { MongooseQueryOptions } from 'mongoose' +import type { MongooseQueryOptions, QueryOptions } from 'mongoose' import type { Document, FindOne, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' export const findOne: FindOne = async function findOne( this: MongooseAdapter, - { collection, joins, locale, req = {} as PayloadRequest, where }, + { collection, joins, locale, req = {} as PayloadRequest, select, where }, ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config @@ -24,6 +25,12 @@ export const findOne: FindOne = async function findOne( where, }) + const projection = buildProjectionFromSelect({ + adapter: this, + fields: collectionConfig.fields, + select, + }) + const aggregate = await buildJoinAggregation({ adapter: this, collection, @@ -31,6 +38,7 @@ export const findOne: FindOne = async function findOne( joins, limit: 1, locale, + projection, query, }) @@ -38,6 +46,7 @@ export const findOne: FindOne = async function findOne( if (aggregate) { ;[doc] = await Model.aggregate(aggregate, options) } else { + ;(options as Record).projection = projection doc = await Model.findOne(query, {}, options) } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 186d938214..1e66303c13 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,11 +1,12 @@ import type { PaginateOptions } from 'mongoose' import type { FindVersions, PayloadRequest } from 'payload' -import { flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' import { buildSortParam } from './queries/buildSortParam.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' @@ -18,6 +19,7 @@ export const findVersions: FindVersions = async function findVersions( page, pagination, req = {} as PayloadRequest, + select, skip, sort: sortArg, where, @@ -65,6 +67,11 @@ export const findVersions: FindVersions = async function findVersions( options, page, pagination, + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields(this.payload.config, collectionConfig), + select, + }), sort, useEstimatedCount, } diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index fca0ec353d..b3ce18767a 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,12 +1,13 @@ import type { PaginateOptions } from 'mongoose' import type { PayloadRequest, QueryDrafts } from 'payload' -import { combineQueries, flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { withSession } from './withSession.js' @@ -20,6 +21,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( page, pagination, req = {} as PayloadRequest, + select, sort: sortArg, where, }, @@ -54,6 +56,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( where: combinedWhere, }) + const projection = buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields(this.payload.config, collectionConfig), + select, + }) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0 @@ -64,6 +71,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( options, page, pagination, + projection, sort, useEstimatedCount, } @@ -109,6 +117,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( joins, limit, locale, + projection, query: versionQuery, versions: true, }) diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 66bcdd4aff..4016a33ee1 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -2,19 +2,23 @@ import type { PayloadRequest, UpdateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { withSession } from './withSession.js' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, - { slug, data, req = {} as PayloadRequest }, + { slug, data, req = {} as PayloadRequest, select }, ) { const Model = this.globals + const fields = this.payload.config.globals.find((global) => global.slug === slug).fields + const options = { ...(await withSession(this, req)), lean: true, new: true, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), } let result @@ -22,7 +26,7 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( const sanitizedData = sanitizeRelationshipIDs({ config: this.payload.config, data, - fields: this.payload.config.globals.find((global) => global.slug === slug).fields, + fields, }) result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index 10618033d4..4588ef5951 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -7,6 +7,7 @@ import { import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { withSession } from './withSession.js' @@ -17,16 +18,23 @@ export async function updateGlobalVersion( global: globalSlug, locale, req = {} as PayloadRequest, + select, versionData, where, }: UpdateGlobalVersionArgs, ) { const VersionModel = this.versions[globalSlug] const whereToUse = where || { id: { equals: id } } + const fields = buildVersionGlobalFields( + this.payload.config, + this.payload.config.globals.find((global) => global.slug === globalSlug), + ) + const options = { ...(await withSession(this, req)), lean: true, new: true, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), } const query = await VersionModel.buildQuery({ @@ -38,10 +46,7 @@ export async function updateGlobalVersion( const sanitizedData = sanitizeRelationshipIDs({ config: this.payload.config, data: versionData, - fields: buildVersionGlobalFields( - this.payload.config, - this.payload.config.globals.find((global) => global.slug === globalSlug), - ), + fields, }) const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index c456b57d26..ce6d04ecac 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -3,6 +3,7 @@ import type { PayloadRequest, UpdateOne } from 'payload' import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { handleError } from './utilities/handleError.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' @@ -17,16 +18,19 @@ export const updateOne: UpdateOne = async function updateOne( locale, options: optionsArgs = {}, req = {} as PayloadRequest, + select, where: whereArg, }, ) { const where = id ? { id: { equals: id } } : whereArg const Model = this.collections[collection] + const fields = this.payload.collections[collection].config.fields const options: QueryOptions = { ...optionsArgs, ...(await withSession(this, req)), lean: true, new: true, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), } const query = await Model.buildQuery({ @@ -40,7 +44,7 @@ export const updateOne: UpdateOne = async function updateOne( const sanitizedData = sanitizeRelationshipIDs({ config: this.payload.config, data, - fields: this.payload.collections[collection].config.fields, + fields, }) try { diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index da45bbb60b..f199d4c7f1 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -2,19 +2,26 @@ import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } import type { MongooseAdapter } from './index.js' +import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { withSession } from './withSession.js' export const updateVersion: UpdateVersion = async function updateVersion( this: MongooseAdapter, - { id, collection, locale, req = {} as PayloadRequest, versionData, where }, + { id, collection, locale, req = {} as PayloadRequest, select, versionData, where }, ) { const VersionModel = this.versions[collection] const whereToUse = where || { id: { equals: id } } + const fields = buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collection].config, + ) + const options = { ...(await withSession(this, req)), lean: true, new: true, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), } const query = await VersionModel.buildQuery({ @@ -26,10 +33,7 @@ export const updateVersion: UpdateVersion = async function updateVersion( const sanitizedData = sanitizeRelationshipIDs({ config: this.payload.config, data: versionData, - fields: buildVersionCollectionFields( - this.payload.config, - this.payload.collections[collection].config, - ), + fields, }) const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) diff --git a/packages/db-mongodb/src/upsert.ts b/packages/db-mongodb/src/upsert.ts index c4d4376e19..8f846c4a23 100644 --- a/packages/db-mongodb/src/upsert.ts +++ b/packages/db-mongodb/src/upsert.ts @@ -4,7 +4,7 @@ import type { MongooseAdapter } from './index.js' export const upsert: Upsert = async function upsert( this: MongooseAdapter, - { collection, data, locale, req = {} as PayloadRequest, where }, + { collection, data, locale, req = {} as PayloadRequest, select, where }, ) { - return this.updateOne({ collection, data, locale, options: { upsert: true }, req, where }) + return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where }) } diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 2ff570a25c..ab0654c596 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -13,6 +13,7 @@ type BuildJoinAggregationArgs = { // the number of docs to get at the top collection level limit?: number locale: string + projection?: Record // the where clause for the top collection query?: Where /** whether the query is from drafts */ @@ -26,6 +27,7 @@ export const buildJoinAggregation = async ({ joins, limit, locale, + projection, query, versions, }: BuildJoinAggregationArgs): Promise => { @@ -56,6 +58,10 @@ export const buildJoinAggregation = async ({ for (const join of joinConfig[slug]) { const joinModel = adapter.collections[join.field.collection] + if (projection && !projection[join.schemaPath]) { + continue + } + const { limit: limitJoin = join.field.defaultLimit ?? 10, sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort, @@ -174,5 +180,9 @@ export const buildJoinAggregation = async ({ } } + if (projection) { + aggregate.push({ $project: projection }) + } + return aggregate } diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts new file mode 100644 index 0000000000..1bb2bb7fea --- /dev/null +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -0,0 +1,234 @@ +import { + deepCopyObjectSimple, + type Field, + type FieldAffectingData, + type SelectMode, + type SelectType, + type TabAsField, +} from 'payload' +import { fieldAffectsData, getSelectMode } from 'payload/shared' + +import type { MongooseAdapter } from '../index.js' + +const addFieldToProjection = ({ + adapter, + databaseSchemaPath, + field, + projection, + withinLocalizedField, +}: { + adapter: MongooseAdapter + databaseSchemaPath: string + field: FieldAffectingData + projection: Record + withinLocalizedField: boolean +}) => { + const { config } = adapter.payload + + if (withinLocalizedField && config.localization) { + for (const locale of config.localization.localeCodes) { + const localeDatabaseSchemaPath = databaseSchemaPath.replace('', locale) + projection[`${localeDatabaseSchemaPath}${field.name}`] = true + } + } else { + projection[`${databaseSchemaPath}${field.name}`] = true + } +} + +const traverseFields = ({ + adapter, + databaseSchemaPath = '', + fields, + projection, + select, + selectAllOnCurrentLevel = false, + selectMode, + withinLocalizedField = false, +}: { + adapter: MongooseAdapter + databaseSchemaPath?: string + fields: (Field | TabAsField)[] + projection: Record + select: SelectType + selectAllOnCurrentLevel?: boolean + selectMode: SelectMode + withinLocalizedField?: boolean +}) => { + for (const field of fields) { + if (fieldAffectsData(field)) { + if (selectMode === 'include') { + if (select[field.name] === true || selectAllOnCurrentLevel) { + addFieldToProjection({ + adapter, + databaseSchemaPath, + field, + projection, + withinLocalizedField, + }) + continue + } + + if (!select[field.name]) { + continue + } + } + + if (selectMode === 'exclude') { + if (typeof select[field.name] === 'undefined') { + addFieldToProjection({ + adapter, + databaseSchemaPath, + field, + projection, + withinLocalizedField, + }) + continue + } + + if (select[field.name] === false) { + continue + } + } + } + + let fieldDatabaseSchemaPath = databaseSchemaPath + let fieldWithinLocalizedField = withinLocalizedField + + if (fieldAffectsData(field)) { + fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.` + + if (field.localized) { + fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}.` + fieldWithinLocalizedField = true + } + } + + switch (field.type) { + case 'collapsible': + case 'row': + traverseFields({ + adapter, + databaseSchemaPath, + fields: field.fields, + projection, + select, + selectMode, + withinLocalizedField, + }) + break + + case 'tabs': + traverseFields({ + adapter, + databaseSchemaPath, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + projection, + select, + selectMode, + withinLocalizedField, + }) + break + + case 'group': + case 'tab': + case 'array': + if (field.type === 'array' && selectMode === 'include') { + select[field.name]['id'] = true + } + + traverseFields({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + fields: field.fields, + projection, + select: select[field.name] as SelectType, + selectMode, + withinLocalizedField: fieldWithinLocalizedField, + }) + + break + + case 'blocks': { + const blocksSelect = select[field.name] as SelectType + + for (const block of field.blocks) { + if ( + (selectMode === 'include' && blocksSelect[block.slug] === true) || + (selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined') + ) { + traverseFields({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + fields: block.fields, + projection, + select: {}, + selectAllOnCurrentLevel: true, + selectMode: 'include', + withinLocalizedField: fieldWithinLocalizedField, + }) + continue + } + + let blockSelectMode = selectMode + + if (selectMode === 'exclude' && blocksSelect[block.slug] === false) { + blockSelectMode = 'include' + } + + if (typeof blocksSelect[block.slug] !== 'object') { + blocksSelect[block.slug] = {} + } + + if (blockSelectMode === 'include') { + blocksSelect[block.slug]['id'] = true + blocksSelect[block.slug]['blockType'] = true + } + + traverseFields({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + fields: block.fields, + projection, + select: blocksSelect[block.slug] as SelectType, + selectMode: blockSelectMode, + withinLocalizedField: fieldWithinLocalizedField, + }) + } + + break + } + + default: + break + } + } +} + +export const buildProjectionFromSelect = ({ + adapter, + fields, + select, +}: { + adapter: MongooseAdapter + fields: Field[] + select?: SelectType +}): Record | undefined => { + if (!select) { + return + } + + const projection: Record = { + _id: true, + } + + traverseFields({ + adapter, + fields, + projection, + // Clone to safely mutate it later + select: deepCopyObjectSimple(select), + selectMode: getSelectMode(select), + }) + + return projection +} diff --git a/packages/drizzle/src/create.ts b/packages/drizzle/src/create.ts index 7ab4679aec..9d40afb01c 100644 --- a/packages/drizzle/src/create.ts +++ b/packages/drizzle/src/create.ts @@ -8,7 +8,7 @@ import { upsertRow } from './upsertRow/index.js' export const create: Create = async function create( this: DrizzleAdapter, - { collection: collectionSlug, data, req }, + { collection: collectionSlug, data, req, select }, ) { const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config @@ -22,6 +22,7 @@ export const create: Create = async function create( fields: collection.fields, operation: 'create', req, + select, tableName, }) diff --git a/packages/drizzle/src/createGlobalVersion.ts b/packages/drizzle/src/createGlobalVersion.ts index d4c72db4bd..999cfb60f8 100644 --- a/packages/drizzle/src/createGlobalVersion.ts +++ b/packages/drizzle/src/createGlobalVersion.ts @@ -16,6 +16,7 @@ export async function createGlobalVersion( globalSlug, publishedLocale, req = {} as PayloadRequest, + select, snapshot, updatedAt, versionData, @@ -41,6 +42,7 @@ export async function createGlobalVersion( fields: buildVersionGlobalFields(this.payload.config, global), operation: 'create', req, + select, tableName, }) diff --git a/packages/drizzle/src/createVersion.ts b/packages/drizzle/src/createVersion.ts index 0eda45ffcb..4f41aa7aca 100644 --- a/packages/drizzle/src/createVersion.ts +++ b/packages/drizzle/src/createVersion.ts @@ -17,6 +17,7 @@ export async function createVersion( parent, publishedLocale, req = {} as PayloadRequest, + select, snapshot, updatedAt, versionData, @@ -51,6 +52,7 @@ export async function createVersion( fields: buildVersionCollectionFields(this.payload.config, collection), operation: 'create', req, + select, tableName, }) diff --git a/packages/drizzle/src/deleteOne.ts b/packages/drizzle/src/deleteOne.ts index 5b49a75538..ca9e330441 100644 --- a/packages/drizzle/src/deleteOne.ts +++ b/packages/drizzle/src/deleteOne.ts @@ -12,7 +12,13 @@ import { transform } from './transform/read/index.js' export const deleteOne: DeleteOne = async function deleteOne( this: DrizzleAdapter, - { collection: collectionSlug, joins: joinQuery, req = {} as PayloadRequest, where: whereArg }, + { + collection: collectionSlug, + joins: joinQuery, + req = {} as PayloadRequest, + select, + where: whereArg, + }, ) { const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config @@ -49,6 +55,7 @@ export const deleteOne: DeleteOne = async function deleteOne( depth: 0, fields: collection.fields, joinQuery, + select, tableName, }) diff --git a/packages/drizzle/src/find.ts b/packages/drizzle/src/find.ts index 9ec5bcf692..66b596ddba 100644 --- a/packages/drizzle/src/find.ts +++ b/packages/drizzle/src/find.ts @@ -16,6 +16,7 @@ export const find: Find = async function find( page = 1, pagination, req = {} as PayloadRequest, + select, sort: sortArg, where, }, @@ -34,6 +35,7 @@ export const find: Find = async function find( page, pagination, req, + select, sort, tableName, where, diff --git a/packages/drizzle/src/find/buildFindManyArgs.ts b/packages/drizzle/src/find/buildFindManyArgs.ts index 418a3da3ba..3614aa84fd 100644 --- a/packages/drizzle/src/find/buildFindManyArgs.ts +++ b/packages/drizzle/src/find/buildFindManyArgs.ts @@ -1,5 +1,7 @@ import type { DBQueryConfig } from 'drizzle-orm' -import type { Field, JoinQuery } from 'payload' +import type { Field, JoinQuery, SelectType } from 'payload' + +import { getSelectMode } from 'payload/shared' import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js' @@ -15,6 +17,7 @@ type BuildFindQueryArgs = { */ joins?: BuildQueryJoinAliases locale?: string + select?: SelectType tableName: string versions?: boolean } @@ -34,6 +37,7 @@ export const buildFindManyArgs = ({ joinQuery, joins = [], locale, + select, tableName, versions, }: BuildFindQueryArgs): Record => { @@ -42,48 +46,30 @@ export const buildFindManyArgs = ({ with: {}, } + if (select) { + result.columns = { + id: true, + } + } + const _locales: Result = { - columns: { - id: false, - _parentID: false, - }, + columns: select + ? { _locale: true } + : { + id: false, + _parentID: false, + }, extras: {}, with: {}, } - if (adapter.tables[`${tableName}_texts`]) { - result.with._texts = { - columns: { - id: false, - parent: false, - }, - orderBy: ({ order }, { asc: ASC }) => [ASC(order)], - } - } - - if (adapter.tables[`${tableName}_numbers`]) { - result.with._numbers = { - columns: { - id: false, - parent: false, - }, - orderBy: ({ order }, { asc: ASC }) => [ASC(order)], - } - } - - if (adapter.tables[`${tableName}${adapter.relationshipsSuffix}`]) { - result.with._rels = { - columns: { - id: false, - parent: false, - }, - orderBy: ({ order }, { asc: ASC }) => [ASC(order)], - } - } - - if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) { - result.with._locales = _locales - } + const withTabledFields = select + ? {} + : { + numbers: true, + rels: true, + texts: true, + } traverseFields({ _locales, @@ -96,11 +82,51 @@ export const buildFindManyArgs = ({ joins, locale, path: '', + select, + selectMode: select ? getSelectMode(select) : undefined, tablePath: '', topLevelArgs: result, topLevelTableName: tableName, versions, + withTabledFields, }) + if (adapter.tables[`${tableName}_texts`] && withTabledFields.texts) { + result.with._texts = { + columns: { + id: false, + parent: false, + }, + orderBy: ({ order }, { asc: ASC }) => [ASC(order)], + } + } + + if (adapter.tables[`${tableName}_numbers`] && withTabledFields.numbers) { + result.with._numbers = { + columns: { + id: false, + parent: false, + }, + orderBy: ({ order }, { asc: ASC }) => [ASC(order)], + } + } + + if (adapter.tables[`${tableName}${adapter.relationshipsSuffix}`] && withTabledFields.rels) { + result.with._rels = { + columns: { + id: false, + parent: false, + }, + orderBy: ({ order }, { asc: ASC }) => [ASC(order)], + } + } + + if ( + adapter.tables[`${tableName}${adapter.localesSuffix}`] && + (!select || Object.keys(_locales.columns).length > 1) + ) { + result.with._locales = _locales + } + return result } diff --git a/packages/drizzle/src/find/findMany.ts b/packages/drizzle/src/find/findMany.ts index 442af7fd77..84491adc97 100644 --- a/packages/drizzle/src/find/findMany.ts +++ b/packages/drizzle/src/find/findMany.ts @@ -26,6 +26,7 @@ export const findMany = async function find({ page = 1, pagination, req = {} as PayloadRequest, + select, skip, sort, tableName, @@ -72,6 +73,7 @@ export const findMany = async function find({ fields, joinQuery, joins, + select, tableName, versions, }) diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 558341179a..7badc56871 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -1,5 +1,5 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql' -import type { Field, JoinQuery } from 'payload' +import type { Field, JoinQuery, SelectMode, SelectType } from 'payload' import { and, eq, sql } from 'drizzle-orm' import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared' @@ -22,10 +22,19 @@ type TraverseFieldArgs = { joins?: BuildQueryJoinAliases locale?: string path: string + select?: SelectType + selectAllOnCurrentLevel?: boolean + selectMode?: SelectMode tablePath: string topLevelArgs: Record topLevelTableName: string versions?: boolean + withinLocalizedField?: boolean + withTabledFields: { + numbers?: boolean + rels?: boolean + texts?: boolean + } } export const traverseFields = ({ @@ -39,10 +48,15 @@ export const traverseFields = ({ joins, locale, path, + select, + selectAllOnCurrentLevel = false, + selectMode, tablePath, topLevelArgs, topLevelTableName, versions, + withinLocalizedField = false, + withTabledFields, }: TraverseFieldArgs) => { fields.forEach((field) => { if (fieldIsVirtual(field)) { @@ -74,9 +88,12 @@ export const traverseFields = ({ joinQuery, joins, path, + select, + selectMode, tablePath, topLevelArgs, topLevelTableName, + withTabledFields, }) return @@ -87,6 +104,20 @@ export const traverseFields = ({ const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath + const tabSelect = tabHasName(tab) ? select?.[tab.name] : select + + if (tabSelect === false) { + return + } + + let tabSelectAllOnCurrentLevel = selectAllOnCurrentLevel + + if (tabHasName(tab) && select && !tabSelectAllOnCurrentLevel) { + tabSelectAllOnCurrentLevel = + select[tab.name] === true || + (selectMode === 'exclude' && typeof select[tab.name] === 'undefined') + } + traverseFields({ _locales, adapter, @@ -97,10 +128,14 @@ export const traverseFields = ({ joinQuery, joins, path: tabPath, + select: typeof tabSelect === 'object' ? tabSelect : undefined, + selectAllOnCurrentLevel: tabSelectAllOnCurrentLevel, + selectMode, tablePath: tabTablePath, topLevelArgs, topLevelTableName, versions, + withTabledFields, }) }) @@ -110,10 +145,27 @@ export const traverseFields = ({ if (fieldAffectsData(field)) { switch (field.type) { case 'array': { + const arraySelect = selectAllOnCurrentLevel ? true : select?.[field.name] + + if (select) { + if ( + (selectMode === 'include' && typeof arraySelect === 'undefined') || + (selectMode === 'exclude' && arraySelect === false) + ) { + break + } + } + const withArray: Result = { - columns: { - _parentID: false, - }, + columns: + typeof arraySelect === 'object' + ? { + id: true, + _order: true, + } + : { + _parentID: false, + }, orderBy: ({ _order }, { asc }) => [asc(_order)], with: {}, } @@ -122,17 +174,33 @@ export const traverseFields = ({ `${currentTableName}_${tablePath}${toSnakeCase(field.name)}`, ) + if (typeof arraySelect === 'object') { + if (adapter.tables[arrayTableName]._locale) { + withArray.columns._locale = true + } + + if (adapter.tables[arrayTableName]._uuid) { + withArray.columns._uuid = true + } + } + const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}` if (adapter.tables[arrayTableNameWithLocales]) { withArray.with._locales = { - columns: { - id: false, - _parentID: false, - }, + columns: + typeof arraySelect === 'object' + ? { + _locale: true, + } + : { + id: false, + _parentID: false, + }, with: {}, } } + currentArgs.with[`${path}${field.name}`] = withArray traverseFields({ @@ -144,16 +212,37 @@ export const traverseFields = ({ fields: field.fields, joinQuery, path: '', + select: typeof arraySelect === 'object' ? arraySelect : undefined, + selectMode, tablePath: '', topLevelArgs, topLevelTableName, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, }) + if ( + typeof arraySelect === 'object' && + withArray.with._locales && + Object.keys(withArray.with._locales).length === 1 + ) { + delete withArray.with._locales + } + break } case 'select': { if (field.hasMany) { + if (select) { + if ( + (selectMode === 'include' && !select[field.name]) || + (selectMode === 'exclude' && select[field.name] === false) + ) { + break + } + } + const withSelect: Result = { columns: { id: false, @@ -169,15 +258,55 @@ export const traverseFields = ({ break } - case 'blocks': + case 'blocks': { + const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name] + + if (select) { + if ( + (selectMode === 'include' && !blocksSelect) || + (selectMode === 'exclude' && blocksSelect === false) + ) { + break + } + } + field.blocks.forEach((block) => { const blockKey = `_blocks_${block.slug}` + let blockSelect: boolean | SelectType | undefined + + let blockSelectMode = selectMode + + if (selectMode === 'include' && blocksSelect === true) { + blockSelect = true + } + + if (typeof blocksSelect === 'object') { + if (typeof blocksSelect[block.slug] === 'object') { + blockSelect = blocksSelect[block.slug] + } else if ( + (selectMode === 'include' && typeof blocksSelect[block.slug] === 'undefined') || + (selectMode === 'exclude' && blocksSelect[block.slug] === false) + ) { + blockSelect = {} + blockSelectMode = 'include' + } else if (selectMode === 'include' && blocksSelect[block.slug] === true) { + blockSelect = true + } + } + if (!topLevelArgs[blockKey]) { const withBlock: Result = { - columns: { - _parentID: false, - }, + columns: + typeof blockSelect === 'object' + ? { + id: true, + _order: true, + _path: true, + } + : { + _parentID: false, + }, orderBy: ({ _order }, { asc }) => [asc(_order)], with: {}, } @@ -186,10 +315,26 @@ export const traverseFields = ({ `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`, ) + if (typeof blockSelect === 'object') { + if (adapter.tables[tableName]._locale) { + withBlock.columns._locale = true + } + + if (adapter.tables[tableName]._uuid) { + withBlock.columns._uuid = true + } + } + if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) { withBlock.with._locales = { with: {}, } + + if (typeof blockSelect === 'object') { + withBlock.with._locales.columns = { + _locale: true, + } + } } topLevelArgs.with[blockKey] = withBlock @@ -202,16 +347,35 @@ export const traverseFields = ({ fields: block.fields, joinQuery, path: '', + select: typeof blockSelect === 'object' ? blockSelect : undefined, + selectMode: blockSelectMode, tablePath: '', topLevelArgs, topLevelTableName, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, }) + + if ( + typeof blockSelect === 'object' && + withBlock.with._locales && + Object.keys(withBlock.with._locales.columns).length === 1 + ) { + delete withBlock.with._locales + } } }) break + } case 'group': { + const groupSelect = select?.[field.name] + + if (groupSelect === false) { + break + } + traverseFields({ _locales, adapter, @@ -222,10 +386,18 @@ export const traverseFields = ({ joinQuery, joins, path: `${path}${field.name}_`, + select: typeof groupSelect === 'object' ? groupSelect : undefined, + selectAllOnCurrentLevel: + selectAllOnCurrentLevel || + groupSelect === true || + (selectMode === 'exclude' && typeof groupSelect === 'undefined'), + selectMode, tablePath: `${tablePath}${toSnakeCase(field.name)}_`, topLevelArgs, topLevelTableName, versions, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, }) break @@ -237,6 +409,13 @@ export const traverseFields = ({ break } + if ( + (select && selectMode === 'include' && !select[field.name]) || + (selectMode === 'exclude' && select[field.name] === false) + ) { + break + } + const { limit: limitArg = field.defaultLimit ?? 10, sort = field.defaultSort, @@ -410,6 +589,40 @@ export const traverseFields = ({ } default: { + if (!select && !selectAllOnCurrentLevel) { + break + } + + if ( + selectAllOnCurrentLevel || + (selectMode === 'include' && select[field.name] === true) || + (selectMode === 'exclude' && typeof select[field.name] === 'undefined') + ) { + const fieldPath = `${path}${field.name}` + + if ((field.localized || withinLocalizedField) && _locales) { + _locales.columns[fieldPath] = true + } else if (adapter.tables[currentTableName]?.[fieldPath]) { + currentArgs.columns[fieldPath] = true + } + + if ( + !withTabledFields.rels && + field.type === 'relationship' && + (field.hasMany || Array.isArray(field.relationTo)) + ) { + withTabledFields.rels = true + } + + if (!withTabledFields.numbers && field.type === 'number' && field.hasMany) { + withTabledFields.numbers = true + } + + if (!withTabledFields.texts && field.type === 'text' && field.hasMany) { + withTabledFields.texts = true + } + } + break } } diff --git a/packages/drizzle/src/findGlobal.ts b/packages/drizzle/src/findGlobal.ts index c624a34b2b..f2873cd68a 100644 --- a/packages/drizzle/src/findGlobal.ts +++ b/packages/drizzle/src/findGlobal.ts @@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js' export const findGlobal: FindGlobal = async function findGlobal( this: DrizzleAdapter, - { slug, locale, req, where }, + { slug, locale, req, select, where }, ) { const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) @@ -23,6 +23,7 @@ export const findGlobal: FindGlobal = async function findGlobal( locale, pagination: false, req, + select, tableName, where, }) diff --git a/packages/drizzle/src/findGlobalVersions.ts b/packages/drizzle/src/findGlobalVersions.ts index bb55ae3604..7e47c62d0c 100644 --- a/packages/drizzle/src/findGlobalVersions.ts +++ b/packages/drizzle/src/findGlobalVersions.ts @@ -16,6 +16,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV page, pagination, req = {} as PayloadRequest, + select, skip, sort: sortArg, where, @@ -40,6 +41,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV page, pagination, req, + select, skip, sort, tableName, diff --git a/packages/drizzle/src/findOne.ts b/packages/drizzle/src/findOne.ts index 78da901a5c..fe8357bc13 100644 --- a/packages/drizzle/src/findOne.ts +++ b/packages/drizzle/src/findOne.ts @@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js' export async function findOne( this: DrizzleAdapter, - { collection, joins, locale, req = {} as PayloadRequest, where }: FindOneArgs, + { collection, joins, locale, req = {} as PayloadRequest, select, where }: FindOneArgs, ): Promise { const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config @@ -23,6 +23,7 @@ export async function findOne( page: 1, pagination: false, req, + select, sort: undefined, tableName, where, diff --git a/packages/drizzle/src/findVersions.ts b/packages/drizzle/src/findVersions.ts index ae0e280370..3c86547342 100644 --- a/packages/drizzle/src/findVersions.ts +++ b/packages/drizzle/src/findVersions.ts @@ -16,6 +16,7 @@ export const findVersions: FindVersions = async function findVersions( page, pagination, req = {} as PayloadRequest, + select, skip, sort: sortArg, where, @@ -38,6 +39,7 @@ export const findVersions: FindVersions = async function findVersions( page, pagination, req, + select, skip, sort, tableName, diff --git a/packages/drizzle/src/queryDrafts.ts b/packages/drizzle/src/queryDrafts.ts index 9c32f1fbd7..654f9eaaf3 100644 --- a/packages/drizzle/src/queryDrafts.ts +++ b/packages/drizzle/src/queryDrafts.ts @@ -1,4 +1,4 @@ -import type { JoinQuery, PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload' +import type { PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload' import { buildVersionCollectionFields, combineQueries } from 'payload' import toSnakeCase from 'to-snake-case' @@ -17,6 +17,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( page = 1, pagination, req = {} as PayloadRequest, + select, sort, where, }, @@ -38,6 +39,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( page, pagination, req, + select, sort, tableName, versions: true, diff --git a/packages/drizzle/src/update.ts b/packages/drizzle/src/update.ts index 0526b78b57..9450564202 100644 --- a/packages/drizzle/src/update.ts +++ b/packages/drizzle/src/update.ts @@ -10,7 +10,7 @@ import { upsertRow } from './upsertRow/index.js' export const updateOne: UpdateOne = async function updateOne( this: DrizzleAdapter, - { id, collection: collectionSlug, data, draft, joins: joinQuery, locale, req, where: whereArg }, + { id, collection: collectionSlug, data, joins: joinQuery, locale, req, select, where: whereArg }, ) { const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config @@ -49,6 +49,7 @@ export const updateOne: UpdateOne = async function updateOne( joinQuery, operation: 'update', req, + select, tableName, }) diff --git a/packages/drizzle/src/updateGlobal.ts b/packages/drizzle/src/updateGlobal.ts index ccb13d47a6..18dfe83fc4 100644 --- a/packages/drizzle/src/updateGlobal.ts +++ b/packages/drizzle/src/updateGlobal.ts @@ -8,7 +8,7 @@ import { upsertRow } from './upsertRow/index.js' export async function updateGlobal>( this: DrizzleAdapter, - { slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs, + { slug, data, req = {} as PayloadRequest, select }: UpdateGlobalArgs, ): Promise { const db = this.sessions[await req?.transactionID]?.db || this.drizzle const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) @@ -23,6 +23,7 @@ export async function updateGlobal>( db, fields: globalConfig.fields, req, + select, tableName, }) diff --git a/packages/drizzle/src/updateGlobalVersion.ts b/packages/drizzle/src/updateGlobalVersion.ts index 3353d885aa..3b61fa57bf 100644 --- a/packages/drizzle/src/updateGlobalVersion.ts +++ b/packages/drizzle/src/updateGlobalVersion.ts @@ -21,6 +21,7 @@ export async function updateGlobalVersion( global, locale, req = {} as PayloadRequest, + select, versionData, where: whereArg, }: UpdateGlobalVersionArgs, @@ -53,6 +54,7 @@ export async function updateGlobalVersion( fields, operation: 'update', req, + select, tableName, where, }) diff --git a/packages/drizzle/src/updateVersion.ts b/packages/drizzle/src/updateVersion.ts index 25e285f907..935623bce9 100644 --- a/packages/drizzle/src/updateVersion.ts +++ b/packages/drizzle/src/updateVersion.ts @@ -21,6 +21,7 @@ export async function updateVersion( collection, locale, req = {} as PayloadRequest, + select, versionData, where: whereArg, }: UpdateVersionArgs, @@ -50,6 +51,7 @@ export async function updateVersion( fields, operation: 'update', req, + select, tableName, where, }) diff --git a/packages/drizzle/src/upsertRow/index.ts b/packages/drizzle/src/upsertRow/index.ts index 722fe88e9d..77c3448c00 100644 --- a/packages/drizzle/src/upsertRow/index.ts +++ b/packages/drizzle/src/upsertRow/index.ts @@ -24,6 +24,7 @@ export const upsertRow = async | TypeWithID>( operation, path = '', req, + select, tableName, upsertTarget, where, @@ -415,6 +416,7 @@ export const upsertRow = async | TypeWithID>( depth: 0, fields, joinQuery, + select, tableName, }) diff --git a/packages/drizzle/src/upsertRow/types.ts b/packages/drizzle/src/upsertRow/types.ts index de29ff0423..81f0aa701e 100644 --- a/packages/drizzle/src/upsertRow/types.ts +++ b/packages/drizzle/src/upsertRow/types.ts @@ -1,5 +1,5 @@ import type { SQL } from 'drizzle-orm' -import type { Field, JoinQuery, PayloadRequest } from 'payload' +import type { Field, JoinQuery, PayloadRequest, SelectType } from 'payload' import type { DrizzleAdapter, DrizzleTransaction, GenericColumn } from '../types.js' @@ -23,6 +23,7 @@ type CreateArgs = { id?: never joinQuery?: never operation: 'create' + select?: SelectType upsertTarget?: never where?: never } & BaseArgs @@ -31,6 +32,7 @@ type UpdateArgs = { id?: number | string joinQuery?: JoinQuery operation: 'update' + select?: SelectType upsertTarget?: GenericColumn where?: SQL } & BaseArgs diff --git a/packages/graphql/src/resolvers/globals/update.ts b/packages/graphql/src/resolvers/globals/update.ts index 1656356dc5..22678d7807 100644 --- a/packages/graphql/src/resolvers/globals/update.ts +++ b/packages/graphql/src/resolvers/globals/update.ts @@ -1,4 +1,10 @@ -import type { DataFromGlobalSlug, GlobalSlug, PayloadRequest, SanitizedGlobalConfig } from 'payload' +import type { + DataFromGlobalSlug, + GlobalSlug, + PayloadRequest, + SanitizedGlobalConfig, + SelectType, +} from 'payload' import type { DeepPartial } from 'ts-essentials' import { isolateObjectProperty, updateOperationGlobal } from 'payload' @@ -40,7 +46,7 @@ export function update( req: isolateObjectProperty(context.req, 'transactionID'), } - const result = await updateOperationGlobal(options) + const result = await updateOperationGlobal(options) return result } } diff --git a/packages/next/src/routes/rest/collections/create.ts b/packages/next/src/routes/rest/collections/create.ts index 191d438548..e388d8c5a0 100644 --- a/packages/next/src/routes/rest/collections/create.ts +++ b/packages/next/src/routes/rest/collections/create.ts @@ -6,6 +6,7 @@ import { isNumber } from 'payload/shared' import type { CollectionRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const create: CollectionRouteHandler = async ({ collection, req }) => { const { searchParams } = req @@ -20,6 +21,7 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => { depth: isNumber(depth) ? depth : undefined, draft, req, + select: sanitizeSelect(req.query.select), }) return Response.json( diff --git a/packages/next/src/routes/rest/collections/delete.ts b/packages/next/src/routes/rest/collections/delete.ts index c005019fa5..5d5db732bc 100644 --- a/packages/next/src/routes/rest/collections/delete.ts +++ b/packages/next/src/routes/rest/collections/delete.ts @@ -8,11 +8,13 @@ import { isNumber } from 'payload/shared' import type { CollectionRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => { - const { depth, overrideLock, where } = req.query as { + const { depth, overrideLock, select, where } = req.query as { depth?: string overrideLock?: string + select?: Record where?: Where } @@ -21,6 +23,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => depth: isNumber(depth) ? Number(depth) : undefined, overrideLock: Boolean(overrideLock === 'true'), req, + select: sanitizeSelect(select), where, }) diff --git a/packages/next/src/routes/rest/collections/deleteByID.ts b/packages/next/src/routes/rest/collections/deleteByID.ts index 2aa1508934..9f4869a6d9 100644 --- a/packages/next/src/routes/rest/collections/deleteByID.ts +++ b/packages/next/src/routes/rest/collections/deleteByID.ts @@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const deleteByID: CollectionRouteHandlerWithID = async ({ id: incomingID, @@ -28,6 +29,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({ depth: isNumber(depth) ? depth : undefined, overrideLock: Boolean(overrideLock === 'true'), req, + select: sanitizeSelect(req.query.select), }) const headers = headersWithCors({ diff --git a/packages/next/src/routes/rest/collections/duplicate.ts b/packages/next/src/routes/rest/collections/duplicate.ts index dddb4d16db..891af1794f 100644 --- a/packages/next/src/routes/rest/collections/duplicate.ts +++ b/packages/next/src/routes/rest/collections/duplicate.ts @@ -7,6 +7,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const duplicate: CollectionRouteHandlerWithID = async ({ id: incomingID, @@ -30,6 +31,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({ depth: isNumber(depth) ? Number(depth) : undefined, draft, req, + select: sanitizeSelect(req.query.select), }) const message = req.t('general:successfullyDuplicated', { diff --git a/packages/next/src/routes/rest/collections/find.ts b/packages/next/src/routes/rest/collections/find.ts index ffa3eb19d9..29fbcc21f1 100644 --- a/packages/next/src/routes/rest/collections/find.ts +++ b/packages/next/src/routes/rest/collections/find.ts @@ -8,14 +8,16 @@ import type { CollectionRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const find: CollectionRouteHandler = async ({ collection, req }) => { - const { depth, draft, joins, limit, page, sort, where } = req.query as { + const { depth, draft, joins, limit, page, select, sort, where } = req.query as { depth?: string draft?: string joins?: JoinQuery limit?: string page?: string + select?: Record sort?: string where?: Where } @@ -28,6 +30,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => { limit: isNumber(limit) ? Number(limit) : undefined, page: isNumber(page) ? Number(page) : undefined, req, + select: sanitizeSelect(select), sort: typeof sort === 'string' ? sort.split(',') : undefined, where, }) diff --git a/packages/next/src/routes/rest/collections/findByID.ts b/packages/next/src/routes/rest/collections/findByID.ts index 55eebedd65..4faeace3c4 100644 --- a/packages/next/src/routes/rest/collections/findByID.ts +++ b/packages/next/src/routes/rest/collections/findByID.ts @@ -9,6 +9,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js' import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findByID: CollectionRouteHandlerWithID = async ({ id: incomingID, @@ -31,6 +32,7 @@ export const findByID: CollectionRouteHandlerWithID = async ({ draft: searchParams.get('draft') === 'true', joins: sanitizeJoinParams(req.query.joins as JoinQuery), req, + select: sanitizeSelect(req.query.select), }) return Response.json(result, { diff --git a/packages/next/src/routes/rest/collections/findVersionByID.ts b/packages/next/src/routes/rest/collections/findVersionByID.ts index dee8567346..7ad31fc93a 100644 --- a/packages/next/src/routes/rest/collections/findVersionByID.ts +++ b/packages/next/src/routes/rest/collections/findVersionByID.ts @@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findVersionByID: CollectionRouteHandlerWithID = async ({ id: incomingID, @@ -26,6 +27,7 @@ export const findVersionByID: CollectionRouteHandlerWithID = async ({ collection, depth: isNumber(depth) ? Number(depth) : undefined, req, + select: sanitizeSelect(req.query.select), }) return Response.json(result, { diff --git a/packages/next/src/routes/rest/collections/findVersions.ts b/packages/next/src/routes/rest/collections/findVersions.ts index 25cf18d241..358c3aacc0 100644 --- a/packages/next/src/routes/rest/collections/findVersions.ts +++ b/packages/next/src/routes/rest/collections/findVersions.ts @@ -7,12 +7,14 @@ import { isNumber } from 'payload/shared' import type { CollectionRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findVersions: CollectionRouteHandler = async ({ collection, req }) => { - const { depth, limit, page, sort, where } = req.query as { + const { depth, limit, page, select, sort, where } = req.query as { depth?: string limit?: string page?: string + select?: Record sort?: string where?: Where } @@ -23,6 +25,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req }) limit: isNumber(limit) ? Number(limit) : undefined, page: isNumber(page) ? Number(page) : undefined, req, + select: sanitizeSelect(select), sort: typeof sort === 'string' ? sort.split(',') : undefined, where, }) diff --git a/packages/next/src/routes/rest/collections/update.ts b/packages/next/src/routes/rest/collections/update.ts index 1ebc070051..70e4366fb9 100644 --- a/packages/next/src/routes/rest/collections/update.ts +++ b/packages/next/src/routes/rest/collections/update.ts @@ -8,13 +8,15 @@ import { isNumber } from 'payload/shared' import type { CollectionRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const update: CollectionRouteHandler = async ({ collection, req }) => { - const { depth, draft, limit, overrideLock, where } = req.query as { + const { depth, draft, limit, overrideLock, select, where } = req.query as { depth?: string draft?: string limit?: string overrideLock?: string + select?: Record where?: Where } @@ -26,6 +28,7 @@ export const update: CollectionRouteHandler = async ({ collection, req }) => { limit: isNumber(limit) ? Number(limit) : undefined, overrideLock: Boolean(overrideLock === 'true'), req, + select: sanitizeSelect(select), where, }) diff --git a/packages/next/src/routes/rest/collections/updateByID.ts b/packages/next/src/routes/rest/collections/updateByID.ts index cf7ca62597..7d4b50fdd7 100644 --- a/packages/next/src/routes/rest/collections/updateByID.ts +++ b/packages/next/src/routes/rest/collections/updateByID.ts @@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const updateByID: CollectionRouteHandlerWithID = async ({ id: incomingID, @@ -35,6 +36,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({ overrideLock: Boolean(overrideLock === 'true'), publishSpecificLocale, req, + select: sanitizeSelect(req.query.select), }) let message = req.t('general:updatedSuccessfully') diff --git a/packages/next/src/routes/rest/globals/findOne.ts b/packages/next/src/routes/rest/globals/findOne.ts index e7d15a453c..115200f90e 100644 --- a/packages/next/src/routes/rest/globals/findOne.ts +++ b/packages/next/src/routes/rest/globals/findOne.ts @@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared' import type { GlobalRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => { const { searchParams } = req @@ -16,6 +17,7 @@ export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => { draft: searchParams.get('draft') === 'true', globalConfig, req, + select: sanitizeSelect(req.query.select), }) return Response.json(result, { diff --git a/packages/next/src/routes/rest/globals/findVersionByID.ts b/packages/next/src/routes/rest/globals/findVersionByID.ts index d3c0b34cae..8fd3fd7735 100644 --- a/packages/next/src/routes/rest/globals/findVersionByID.ts +++ b/packages/next/src/routes/rest/globals/findVersionByID.ts @@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared' import type { GlobalRouteHandlerWithID } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => { const { searchParams } = req @@ -15,6 +16,7 @@ export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConf depth: isNumber(depth) ? Number(depth) : undefined, globalConfig, req, + select: sanitizeSelect(req.query.select), }) return Response.json(result, { diff --git a/packages/next/src/routes/rest/globals/findVersions.ts b/packages/next/src/routes/rest/globals/findVersions.ts index d868cd44f3..095cf99d3f 100644 --- a/packages/next/src/routes/rest/globals/findVersions.ts +++ b/packages/next/src/routes/rest/globals/findVersions.ts @@ -7,12 +7,14 @@ import { isNumber } from 'payload/shared' import type { GlobalRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => { - const { depth, limit, page, sort, where } = req.query as { + const { depth, limit, page, select, sort, where } = req.query as { depth?: string limit?: string page?: string + select?: Record sort?: string where?: Where } @@ -23,6 +25,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => limit: isNumber(limit) ? Number(limit) : undefined, page: isNumber(page) ? Number(page) : undefined, req, + select: sanitizeSelect(select), sort: typeof sort === 'string' ? sort.split(',') : undefined, where, }) diff --git a/packages/next/src/routes/rest/globals/update.ts b/packages/next/src/routes/rest/globals/update.ts index afc8e70add..5007de78a1 100644 --- a/packages/next/src/routes/rest/globals/update.ts +++ b/packages/next/src/routes/rest/globals/update.ts @@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared' import type { GlobalRouteHandler } from '../types.js' import { headersWithCors } from '../../../utilities/headersWithCors.js' +import { sanitizeSelect } from '../utilities/sanitizeSelect.js' export const update: GlobalRouteHandler = async ({ globalConfig, req }) => { const { searchParams } = req @@ -22,6 +23,7 @@ export const update: GlobalRouteHandler = async ({ globalConfig, req }) => { globalConfig, publishSpecificLocale, req, + select: sanitizeSelect(req.query.select), }) let message = req.t('general:updatedSuccessfully') diff --git a/packages/next/src/routes/rest/utilities/sanitizeSelect.ts b/packages/next/src/routes/rest/utilities/sanitizeSelect.ts new file mode 100644 index 0000000000..a837ffc6d1 --- /dev/null +++ b/packages/next/src/routes/rest/utilities/sanitizeSelect.ts @@ -0,0 +1,20 @@ +import type { SelectType } from 'payload' + +/** + * Sanitizes REST select query to SelectType + */ +export const sanitizeSelect = (unsanitizedSelect: unknown): SelectType | undefined => { + if (unsanitizedSelect && typeof unsanitizedSelect === 'object') { + for (const k in unsanitizedSelect) { + if (unsanitizedSelect[k] === 'true') { + unsanitizedSelect[k] = true + } else if (unsanitizedSelect[k] === 'false') { + unsanitizedSelect[k] = false + } else if (typeof unsanitizedSelect[k] === 'object') { + sanitizeSelect(unsanitizedSelect[k]) + } + } + } + + return unsanitizedSelect as SelectType +} diff --git a/packages/payload/src/auth/operations/registerFirstUser.ts b/packages/payload/src/auth/operations/registerFirstUser.ts index 66ce4d7275..2030726e3c 100644 --- a/packages/payload/src/auth/operations/registerFirstUser.ts +++ b/packages/payload/src/auth/operations/registerFirstUser.ts @@ -5,7 +5,7 @@ import type { RequiredDataFromCollectionSlug, } from '../../collections/config/types.js' import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' +import type { PayloadRequest, SelectType } from '../../types/index.js' import { Forbidden } from '../../errors/index.js' import { commitTransaction } from '../../utilities/commitTransaction.js' @@ -66,7 +66,7 @@ export const registerFirstUserOperation = async ( // Register first user // ///////////////////////////////////// - const result = await payload.create({ + const result = await payload.create({ collection: slug as TSlug, data, overrideAccess: true, diff --git a/packages/payload/src/auth/strategies/local/register.ts b/packages/payload/src/auth/strategies/local/register.ts index 1ea35110fd..75b2603c10 100644 --- a/packages/payload/src/auth/strategies/local/register.ts +++ b/packages/payload/src/auth/strategies/local/register.ts @@ -1,6 +1,6 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/types.js' import type { JsonObject, Payload } from '../../../index.js' -import type { PayloadRequest, Where } from '../../../types/index.js' +import type { PayloadRequest, SelectType, Where } from '../../../types/index.js' import { ValidationError } from '../../../errors/index.js' import { generatePasswordSaltHash } from './generatePasswordSaltHash.js' @@ -11,6 +11,7 @@ type Args = { password: string payload: Payload req: PayloadRequest + select?: SelectType } export const registerLocalStrategy = async ({ @@ -19,6 +20,7 @@ export const registerLocalStrategy = async ({ password, payload, req, + select, }: Args): Promise> => { const loginWithUsername = collection?.auth?.loginWithUsername @@ -90,5 +92,6 @@ export const registerLocalStrategy = async ({ salt, }, req, + select, }) } diff --git a/packages/payload/src/bin/generateTypes.ts b/packages/payload/src/bin/generateTypes.ts index dc367ecff1..d2d56035b7 100644 --- a/packages/payload/src/bin/generateTypes.ts +++ b/packages/payload/src/bin/generateTypes.ts @@ -3,6 +3,7 @@ import { compile } from 'json-schema-to-typescript' import type { SanitizedConfig } from '../config/types.js' +import { addSelectGenericsToGeneratedTypes } from '../utilities/addSelectGenericsToGeneretedTypes.js' import { configToJSONSchema } from '../utilities/configToJSONSchema.js' import { getLogger } from '../utilities/logger.js' @@ -36,6 +37,8 @@ export async function generateTypes( unreachableDefinitions: true, }) + compiled = addSelectGenericsToGeneratedTypes({ compiledGeneratedTypes: compiled }) + if (config.typescript.declare !== false) { if (config.typescript.declare?.ignoreTSError) { compiled += `\n\n${declareWithTSIgnoreError}` diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index b0e1723d5b..4a83e98749 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -37,8 +37,15 @@ import type { JsonObject, TypedAuthOperations, TypedCollection, + TypedCollectionSelect, } from '../../index.js' -import type { PayloadRequest, RequestContext, Sort } from '../../types/index.js' +import type { + PayloadRequest, + RequestContext, + SelectType, + Sort, + TransformCollectionWithSelect, +} from '../../types/index.js' import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js' import type { IncomingCollectionVersions, @@ -47,6 +54,9 @@ import type { import type { AfterOperationArg, AfterOperationMap } from '../operations/utils.js' export type DataFromCollectionSlug = TypedCollection[TSlug] + +export type SelectFromCollectionSlug = TypedCollectionSelect[TSlug] + export type AuthOperationsFromCollectionSlug = TypedAuthOperations[TSlug] @@ -523,8 +533,8 @@ export type Collection = { } } -export type BulkOperationResult = { - docs: DataFromCollectionSlug[] +export type BulkOperationResult = { + docs: TransformCollectionWithSelect[] errors: { id: DataFromCollectionSlug['id'] message: string diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index bb223a100d..ede23e3316 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -1,14 +1,19 @@ import crypto from 'crypto' import type { CollectionSlug, JsonObject } from '../../index.js' -import type { Document, PayloadRequest } from '../../types/index.js' +import type { + Document, + PayloadRequest, + SelectType, + TransformCollectionWithSelect, +} from '../../types/index.js' import type { AfterChangeHook, BeforeOperationHook, BeforeValidateHook, Collection, - DataFromCollectionSlug, RequiredDataFromCollectionSlug, + SelectFromCollectionSlug, } from '../config/types.js' import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js' @@ -40,12 +45,16 @@ export type Arguments = { overrideAccess?: boolean overwriteExistingFiles?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } -export const createOperation = async ( +export const createOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise> => { +): Promise> => { let args = incomingArgs try { @@ -95,6 +104,7 @@ export const createOperation = async ( payload: { config, email }, }, req, + select, showHiddenFields, } = args @@ -235,12 +245,14 @@ export const createOperation = async ( password: data.password as string, payload: req.payload, req, + select, }) } else { doc = await payload.db.create({ collection: collectionConfig.slug, data: resultWithLocales, req, + select, }) } @@ -293,6 +305,7 @@ export const createOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/delete.ts b/packages/payload/src/collections/operations/delete.ts index 9eabf12f7a..569a7f635c 100644 --- a/packages/payload/src/collections/operations/delete.ts +++ b/packages/payload/src/collections/operations/delete.ts @@ -2,8 +2,14 @@ import httpStatus from 'http-status' import type { AccessResult } from '../../config/types.js' import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest, Where } from '../../types/index.js' -import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js' +import type { PayloadRequest, SelectType, Where } from '../../types/index.js' +import type { + BeforeOperationHook, + BulkOperationResult, + Collection, + DataFromCollectionSlug, + SelectFromCollectionSlug, +} from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { combineQueries } from '../../database/combineQueries.js' @@ -26,19 +32,17 @@ export type Arguments = { overrideAccess?: boolean overrideLock?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean where: Where } -export const deleteOperation = async ( +export const deleteOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise<{ - docs: DataFromCollectionSlug[] - errors: { - id: DataFromCollectionSlug['id'] - message: string - }[] -}> => { +): Promise> => { let args = incomingArgs try { @@ -75,6 +79,7 @@ export const deleteOperation = async ( payload, }, req, + select, showHiddenFields, where, } = args @@ -110,6 +115,7 @@ export const deleteOperation = async ( collection: collectionConfig.slug, locale, req, + select, where: fullWhere, }) @@ -198,6 +204,7 @@ export const deleteOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/deleteByID.ts b/packages/payload/src/collections/operations/deleteByID.ts index eced5be71b..25be137b7e 100644 --- a/packages/payload/src/collections/operations/deleteByID.ts +++ b/packages/payload/src/collections/operations/deleteByID.ts @@ -1,5 +1,9 @@ import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' +import type { + PayloadRequest, + SelectType, + TransformCollectionWithSelect, +} from '../../types/index.js' import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' @@ -24,12 +28,13 @@ export type Arguments = { overrideAccess?: boolean overrideLock?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } -export const deleteByIDOperation = async ( +export const deleteByIDOperation = async ( incomingArgs: Arguments, -): Promise> => { +): Promise> => { let args = incomingArgs try { @@ -68,6 +73,7 @@ export const deleteByIDOperation = async ( payload, }, req, + select, showHiddenFields, } = args @@ -153,6 +159,7 @@ export const deleteByIDOperation = async ( let result: DataFromCollectionSlug = await req.payload.db.deleteOne({ collection: collectionConfig.slug, req, + select, where: { id: { equals: id } }, }) @@ -182,6 +189,7 @@ export const deleteByIDOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) @@ -237,7 +245,7 @@ export const deleteByIDOperation = async ( await commitTransaction(req) } - return result + return result as TransformCollectionWithSelect } catch (error: unknown) { await killTransaction(args.req) throw error diff --git a/packages/payload/src/collections/operations/duplicate.ts b/packages/payload/src/collections/operations/duplicate.ts index 4fe5e4c588..66891db00b 100644 --- a/packages/payload/src/collections/operations/duplicate.ts +++ b/packages/payload/src/collections/operations/duplicate.ts @@ -4,8 +4,16 @@ import httpStatus from 'http-status' import type { FindOneArgs } from '../../database/types.js' import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' -import type { Collection, DataFromCollectionSlug } from '../config/types.js' +import type { + PayloadRequest, + SelectType, + TransformCollectionWithSelect, +} from '../../types/index.js' +import type { + Collection, + DataFromCollectionSlug, + SelectFromCollectionSlug, +} from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { hasWhereAccessResult } from '../../auth/types.js' @@ -21,6 +29,7 @@ import { uploadFiles } from '../../uploads/uploadFiles.js' import { commitTransaction } from '../../utilities/commitTransaction.js' import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' +import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js' import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js' import { saveVersion } from '../../versions/saveVersion.js' import { buildAfterOperation } from './utils.js' @@ -33,12 +42,16 @@ export type Arguments = { id: number | string overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } -export const duplicateOperation = async ( +export const duplicateOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise> => { +): Promise> => { let args = incomingArgs const operation = 'create' @@ -70,6 +83,7 @@ export const duplicateOperation = async ( overrideAccess, req: { fallbackLocale, locale: localeArg, payload }, req, + select, showHiddenFields, } = args @@ -254,12 +268,15 @@ export const duplicateOperation = async ( await uploadFiles(payload, filesToUpload, req) } - const versionDoc = await payload.db.create({ + let versionDoc = await payload.db.create({ collection: collectionConfig.slug, data: result, req, + select, }) + versionDoc = sanitizeInternalFields(versionDoc) + // ///////////////////////////////////// // Create version // ///////////////////////////////////// @@ -290,6 +307,7 @@ export const duplicateOperation = async ( locale: localeArg, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index 140d9af3eb..b7735f0754 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -1,8 +1,18 @@ import type { AccessResult } from '../../config/types.js' import type { PaginatedDocs } from '../../database/types.js' import type { CollectionSlug, JoinQuery } from '../../index.js' -import type { PayloadRequest, Sort, Where } from '../../types/index.js' -import type { Collection, DataFromCollectionSlug } from '../config/types.js' +import type { + PayloadRequest, + SelectType, + Sort, + TransformCollectionWithSelect, + Where, +} from '../../types/index.js' +import type { + Collection, + DataFromCollectionSlug, + SelectFromCollectionSlug, +} from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { combineQueries } from '../../database/combineQueries.js' @@ -11,6 +21,7 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js' import { killTransaction } from '../../utilities/killTransaction.js' import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js' import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js' +import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js' import { getQueryDraftsSort } from '../../versions/drafts/getQueryDraftsSort.js' import { buildAfterOperation } from './utils.js' @@ -27,14 +38,18 @@ export type Arguments = { page?: number pagination?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean sort?: Sort where?: Where } -export const findOperation = async ( +export const findOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise>> => { +): Promise>> => { let args = incomingArgs try { @@ -70,6 +85,7 @@ export const findOperation = async ( pagination = true, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, sort, where, @@ -132,6 +148,7 @@ export const findOperation = async ( page: sanitizedPage, pagination: usePagination, req, + select: getQueryDraftsSelect({ select }), sort: getQueryDraftsSort({ collectionConfig, sort }), where: fullWhere, }) @@ -151,6 +168,7 @@ export const findOperation = async ( page: sanitizedPage, pagination, req, + select, sort, where: fullWhere, }) @@ -268,6 +286,7 @@ export const findOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }), ), @@ -318,7 +337,7 @@ export const findOperation = async ( // Return results // ///////////////////////////////////// - return result + return result as PaginatedDocs> } catch (error: unknown) { await killTransaction(args.req) throw error diff --git a/packages/payload/src/collections/operations/findByID.ts b/packages/payload/src/collections/operations/findByID.ts index cfb74de5d1..0f8394d43a 100644 --- a/packages/payload/src/collections/operations/findByID.ts +++ b/packages/payload/src/collections/operations/findByID.ts @@ -1,7 +1,16 @@ import type { FindOneArgs } from '../../database/types.js' import type { CollectionSlug, JoinQuery } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' -import type { Collection, DataFromCollectionSlug } from '../config/types.js' +import type { + ApplyDisableErrors, + PayloadRequest, + SelectType, + TransformCollectionWithSelect, +} from '../../types/index.js' +import type { + Collection, + DataFromCollectionSlug, + SelectFromCollectionSlug, +} from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { combineQueries } from '../../database/combineQueries.js' @@ -22,12 +31,17 @@ export type Arguments = { joins?: JoinQuery overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } -export const findByIDOperation = async ( +export const findByIDOperation = async < + TSlug extends CollectionSlug, + TDisableErrors extends boolean, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise> => { +): Promise, TDisableErrors>> => { let args = incomingArgs try { @@ -60,6 +74,7 @@ export const findByIDOperation = async ( overrideAccess = false, req: { fallbackLocale, locale, t }, req, + select, showHiddenFields, } = args @@ -83,6 +98,7 @@ export const findByIDOperation = async ( req: { transactionID: req.transactionID, } as PayloadRequest, + select, where: combineQueries({ id: { equals: id } }, accessResult), } @@ -170,6 +186,7 @@ export const findByIDOperation = async ( entityType: 'collection', overrideAccess, req, + select, }) } @@ -206,6 +223,7 @@ export const findByIDOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) @@ -241,7 +259,10 @@ export const findByIDOperation = async ( // Return results // ///////////////////////////////////// - return result + return result as ApplyDisableErrors< + TransformCollectionWithSelect, + TDisableErrors + > } catch (error: unknown) { await killTransaction(args.req) throw error diff --git a/packages/payload/src/collections/operations/findVersionByID.ts b/packages/payload/src/collections/operations/findVersionByID.ts index b08b795c13..89de00d6c2 100644 --- a/packages/payload/src/collections/operations/findVersionByID.ts +++ b/packages/payload/src/collections/operations/findVersionByID.ts @@ -1,6 +1,6 @@ import httpStatus from 'http-status' -import type { PayloadRequest } from '../../types/index.js' +import type { PayloadRequest, SelectType } from '../../types/index.js' import type { TypeWithVersion } from '../../versions/types.js' import type { Collection, TypeWithID } from '../config/types.js' @@ -18,6 +18,7 @@ export type Arguments = { id: number | string overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } @@ -33,6 +34,7 @@ export const findVersionByIDOperation = async ( overrideAccess, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, } = args @@ -68,6 +70,7 @@ export const findVersionByIDOperation = async ( locale, pagination: false, req, + select, where: fullWhere, }) @@ -119,6 +122,7 @@ export const findVersionByIDOperation = async ( locale, overrideAccess, req, + select: typeof select?.version === 'object' ? select.version : undefined, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/findVersions.ts b/packages/payload/src/collections/operations/findVersions.ts index e0ee8d0b56..beef8e9ca8 100644 --- a/packages/payload/src/collections/operations/findVersions.ts +++ b/packages/payload/src/collections/operations/findVersions.ts @@ -1,5 +1,5 @@ import type { PaginatedDocs } from '../../database/types.js' -import type { PayloadRequest, Sort, Where } from '../../types/index.js' +import type { PayloadRequest, SelectType, Sort, Where } from '../../types/index.js' import type { TypeWithVersion } from '../../versions/types.js' import type { Collection } from '../config/types.js' @@ -19,6 +19,7 @@ export type Arguments = { page?: number pagination?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean sort?: Sort where?: Where @@ -36,6 +37,7 @@ export const findVersionsOperation = async pagination = true, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, sort, where, @@ -75,6 +77,7 @@ export const findVersionsOperation = async page: page || 1, pagination, req, + select, sort, where: fullWhere, }) @@ -127,6 +130,7 @@ export const findVersionsOperation = async locale, overrideAccess, req, + select: typeof select?.version === 'object' ? select.version : undefined, showHiddenFields, }), })), diff --git a/packages/payload/src/collections/operations/local/create.ts b/packages/payload/src/collections/operations/local/create.ts index 7a63c327c8..978c08e265 100644 --- a/packages/payload/src/collections/operations/local/create.ts +++ b/packages/payload/src/collections/operations/local/create.ts @@ -1,14 +1,23 @@ import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + TransformCollectionWithSelect, +} from '../../../types/index.js' import type { File } from '../../../uploads/types.js' -import type { DataFromCollectionSlug, RequiredDataFromCollectionSlug } from '../../config/types.js' +import type { + RequiredDataFromCollectionSlug, + SelectFromCollectionSlug, +} from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { getFileByPath } from '../../../uploads/getFileByPath.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { createOperation } from '../create.js' -export type Options = { +export type Options = { collection: TSlug /** * context, which will then be passed to req.context, which can be read by hooks @@ -26,15 +35,19 @@ export type Options = { overrideAccess?: boolean overwriteExistingFiles?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean user?: Document } // eslint-disable-next-line no-restricted-exports -export default async function createLocal( +export default async function createLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise> { + options: Options, +): Promise> { const { collection: collectionSlug, data, @@ -46,6 +59,7 @@ export default async function createLocal( filePath, overrideAccess = true, overwriteExistingFiles = false, + select, showHiddenFields, } = options const collection = payload.collections[collectionSlug] @@ -59,7 +73,7 @@ export default async function createLocal( const req = await createLocalReq(options, payload) req.file = file ?? (await getFileByPath(filePath)) - return createOperation({ + return createOperation({ collection, data, depth, @@ -69,6 +83,7 @@ export default async function createLocal( overrideAccess, overwriteExistingFiles, req, + select, showHiddenFields, }) } diff --git a/packages/payload/src/collections/operations/local/delete.ts b/packages/payload/src/collections/operations/local/delete.ts index 8d9191faad..c57278cb27 100644 --- a/packages/payload/src/collections/operations/local/delete.ts +++ b/packages/payload/src/collections/operations/local/delete.ts @@ -1,13 +1,20 @@ import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js' -import type { BulkOperationResult, DataFromCollectionSlug } from '../../config/types.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + TransformCollectionWithSelect, + Where, +} from '../../../types/index.js' +import type { BulkOperationResult, SelectFromCollectionSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { deleteOperation } from '../delete.js' import { deleteByIDOperation } from '../deleteByID.js' -export type BaseOptions = { +export type BaseOptions = { collection: TSlug /** * context, which will then be passed to req.context, which can be read by hooks @@ -20,38 +27,60 @@ export type BaseOptions = { overrideAccess?: boolean overrideLock?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean user?: Document } -export type ByIDOptions = { +export type ByIDOptions< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = { id: number | string where?: never -} & BaseOptions +} & BaseOptions -export type ManyOptions = { +export type ManyOptions< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = { id?: never where: Where -} & BaseOptions +} & BaseOptions -export type Options = ByIDOptions | ManyOptions +export type Options< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = ByIDOptions | ManyOptions -async function deleteLocal( +async function deleteLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: ByIDOptions, -): Promise> -async function deleteLocal( + options: ByIDOptions, +): Promise> +async function deleteLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: ManyOptions, -): Promise> -async function deleteLocal( + options: ManyOptions, +): Promise> +async function deleteLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise | DataFromCollectionSlug> -async function deleteLocal( + options: Options, +): Promise | TransformCollectionWithSelect> +async function deleteLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise | DataFromCollectionSlug> { + options: Options, +): Promise | TransformCollectionWithSelect> { const { id, collection: collectionSlug, @@ -59,6 +88,7 @@ async function deleteLocal( disableTransaction, overrideAccess = true, overrideLock, + select, showHiddenFields, where, } = options @@ -79,14 +109,15 @@ async function deleteLocal( overrideAccess, overrideLock, req: await createLocalReq(options, payload), + select, showHiddenFields, where, } if (options.id) { - return deleteByIDOperation(args) + return deleteByIDOperation(args) } - return deleteOperation(args) + return deleteOperation(args) } export default deleteLocal diff --git a/packages/payload/src/collections/operations/local/duplicate.ts b/packages/payload/src/collections/operations/local/duplicate.ts index 5e7097f6ab..c79381b08f 100644 --- a/packages/payload/src/collections/operations/local/duplicate.ts +++ b/packages/payload/src/collections/operations/local/duplicate.ts @@ -1,13 +1,19 @@ import type { CollectionSlug, TypedLocale } from '../../..//index.js' import type { Payload } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js' -import type { DataFromCollectionSlug } from '../../config/types.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + TransformCollectionWithSelect, +} from '../../../types/index.js' +import type { DataFromCollectionSlug, SelectFromCollectionSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { duplicateOperation } from '../duplicate.js' -export type Options = { +export type Options = { collection: TSlug /** * context, which will then be passed to req.context, which can be read by hooks @@ -21,14 +27,18 @@ export type Options = { locale?: TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean user?: Document } -export async function duplicate( +export async function duplicate< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise> { + options: Options, +): Promise> { const { id, collection: collectionSlug, @@ -36,6 +46,7 @@ export async function duplicate( disableTransaction, draft, overrideAccess = true, + select, showHiddenFields, } = options const collection = payload.collections[collectionSlug] @@ -55,7 +66,7 @@ export async function duplicate( const req = await createLocalReq(options, payload) - return duplicateOperation({ + return duplicateOperation({ id, collection, depth, @@ -63,6 +74,7 @@ export async function duplicate( draft, overrideAccess, req, + select, showHiddenFields, }) } diff --git a/packages/payload/src/collections/operations/local/find.ts b/packages/payload/src/collections/operations/local/find.ts index 8f5d545f8e..b6a75fba7f 100644 --- a/packages/payload/src/collections/operations/local/find.ts +++ b/packages/payload/src/collections/operations/local/find.ts @@ -1,13 +1,21 @@ import type { PaginatedDocs } from '../../../database/types.js' import type { CollectionSlug, JoinQuery, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext, Sort, Where } from '../../../types/index.js' -import type { DataFromCollectionSlug } from '../../config/types.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + Sort, + TransformCollectionWithSelect, + Where, +} from '../../../types/index.js' +import type { SelectFromCollectionSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { findOperation } from '../find.js' -export type Options = { +export type Options = { collection: TSlug /** * context, which will then be passed to req.context, which can be read by hooks @@ -26,16 +34,20 @@ export type Options = { page?: number pagination?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean sort?: Sort user?: Document where?: Where } -export async function findLocal( +export async function findLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise>> { + options: Options, +): Promise>> { const { collection: collectionSlug, currentDepth, @@ -48,6 +60,8 @@ export async function findLocal( overrideAccess = true, page, pagination = true, + select, + // select, showHiddenFields, sort, where, @@ -61,7 +75,7 @@ export async function findLocal( ) } - return findOperation({ + return findOperation({ collection, currentDepth, depth, @@ -74,6 +88,7 @@ export async function findLocal( page, pagination, req: await createLocalReq(options, payload), + select, showHiddenFields, sort, where, diff --git a/packages/payload/src/collections/operations/local/findByID.ts b/packages/payload/src/collections/operations/local/findByID.ts index 2c330bc736..20978c08f7 100644 --- a/packages/payload/src/collections/operations/local/findByID.ts +++ b/packages/payload/src/collections/operations/local/findByID.ts @@ -1,12 +1,23 @@ -import type { CollectionSlug, JoinQuery, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js' -import type { DataFromCollectionSlug } from '../../config/types.js' +/* eslint-disable no-restricted-exports */ +import type { CollectionSlug, JoinQuery, Payload, SelectType, TypedLocale } from '../../../index.js' +import type { + ApplyDisableErrors, + Document, + PayloadRequest, + RequestContext, + TransformCollectionWithSelect, +} from '../../../types/index.js' +import type { SelectFromCollectionSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { findByIDOperation } from '../findByID.js' -export type Options = { +export type Options< + TSlug extends CollectionSlug, + TDisableErrors extends boolean, + TSelect extends SelectType, +> = { collection: TSlug /** * context, which will then be passed to req.context, which can be read by hooks @@ -14,7 +25,7 @@ export type Options = { context?: RequestContext currentDepth?: number depth?: number - disableErrors?: boolean + disableErrors?: TDisableErrors draft?: boolean fallbackLocale?: TypedLocale id: number | string @@ -23,18 +34,19 @@ export type Options = { locale?: 'all' | TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean user?: Document } -export default async function findByIDLocal( +export default async function findByIDLocal< + TSlug extends CollectionSlug, + TDisableErrors extends boolean, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: TOptions, -): Promise< - TOptions['disableErrors'] extends true - ? DataFromCollectionSlug | null - : DataFromCollectionSlug -> { + options: Options, +): Promise, TDisableErrors>> { const { id, collection: collectionSlug, @@ -45,6 +57,7 @@ export default async function findByIDLocal( includeLockStatus, joins, overrideAccess = true, + select, showHiddenFields, } = options @@ -56,7 +69,7 @@ export default async function findByIDLocal( ) } - return findByIDOperation({ + return findByIDOperation({ id, collection, currentDepth, @@ -67,6 +80,7 @@ export default async function findByIDLocal( joins, overrideAccess, req: await createLocalReq(options, payload), + select, showHiddenFields, }) } diff --git a/packages/payload/src/collections/operations/local/findVersionByID.ts b/packages/payload/src/collections/operations/local/findVersionByID.ts index 53de77b650..e672d2a578 100644 --- a/packages/payload/src/collections/operations/local/findVersionByID.ts +++ b/packages/payload/src/collections/operations/local/findVersionByID.ts @@ -1,5 +1,5 @@ import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { Document, PayloadRequest, RequestContext, SelectType } from '../../../types/index.js' import type { TypeWithVersion } from '../../../versions/types.js' import type { DataFromCollectionSlug } from '../../config/types.js' @@ -21,6 +21,7 @@ export type Options = { locale?: 'all' | TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean user?: Document } @@ -35,6 +36,7 @@ export default async function findVersionByIDLocal depth, disableErrors = false, overrideAccess = true, + select, showHiddenFields, } = options @@ -55,6 +57,7 @@ export default async function findVersionByIDLocal disableErrors, overrideAccess, req: await createLocalReq(options, payload), + select, showHiddenFields, }) } diff --git a/packages/payload/src/collections/operations/local/findVersions.ts b/packages/payload/src/collections/operations/local/findVersions.ts index f5cbbe5a96..e6ab0048d5 100644 --- a/packages/payload/src/collections/operations/local/findVersions.ts +++ b/packages/payload/src/collections/operations/local/findVersions.ts @@ -1,6 +1,13 @@ import type { PaginatedDocs } from '../../../database/types.js' import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext, Sort, Where } from '../../../types/index.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + Sort, + Where, +} from '../../../types/index.js' import type { TypeWithVersion } from '../../../versions/types.js' import type { DataFromCollectionSlug } from '../../config/types.js' @@ -22,6 +29,7 @@ export type Options = { overrideAccess?: boolean page?: number req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean sort?: Sort user?: Document @@ -38,6 +46,7 @@ export default async function findVersionsLocal( limit, overrideAccess = true, page, + select, showHiddenFields, sort, where, @@ -58,6 +67,7 @@ export default async function findVersionsLocal( overrideAccess, page, req: await createLocalReq(options, payload), + select, showHiddenFields, sort, where, diff --git a/packages/payload/src/collections/operations/local/index.ts b/packages/payload/src/collections/operations/local/index.ts index 24081a222d..c8e878a249 100644 --- a/packages/payload/src/collections/operations/local/index.ts +++ b/packages/payload/src/collections/operations/local/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-exports */ import auth from '../../../auth/operations/local/index.js' import count from './count.js' import create from './create.js' diff --git a/packages/payload/src/collections/operations/local/restoreVersion.ts b/packages/payload/src/collections/operations/local/restoreVersion.ts index fc2b6bfe0a..6e671dbf89 100644 --- a/packages/payload/src/collections/operations/local/restoreVersion.ts +++ b/packages/payload/src/collections/operations/local/restoreVersion.ts @@ -1,5 +1,5 @@ import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { Document, PayloadRequest, RequestContext, SelectType } from '../../../types/index.js' import type { DataFromCollectionSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' @@ -19,6 +19,7 @@ export type Options = { locale?: TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean user?: Document } @@ -27,7 +28,14 @@ export default async function restoreVersionLocal( payload: Payload, options: Options, ): Promise> { - const { id, collection: collectionSlug, depth, overrideAccess = true, showHiddenFields } = options + const { + id, + collection: collectionSlug, + depth, + overrideAccess = true, + select, + showHiddenFields, + } = options const collection = payload.collections[collectionSlug] @@ -46,6 +54,7 @@ export default async function restoreVersionLocal( overrideAccess, payload, req: await createLocalReq(options, payload), + select, showHiddenFields, } diff --git a/packages/payload/src/collections/operations/local/update.ts b/packages/payload/src/collections/operations/local/update.ts index 502ab87fdb..2f13f33593 100644 --- a/packages/payload/src/collections/operations/local/update.ts +++ b/packages/payload/src/collections/operations/local/update.ts @@ -1,12 +1,19 @@ import type { DeepPartial } from 'ts-essentials' import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js' +import type { + Document, + PayloadRequest, + RequestContext, + SelectType, + TransformCollectionWithSelect, + Where, +} from '../../../types/index.js' import type { File } from '../../../uploads/types.js' import type { BulkOperationResult, - DataFromCollectionSlug, RequiredDataFromCollectionSlug, + SelectFromCollectionSlug, } from '../../config/types.js' import { APIError } from '../../../errors/index.js' @@ -15,7 +22,7 @@ import { createLocalReq } from '../../../utilities/createLocalReq.js' import { updateOperation } from '../update.js' import { updateByIDOperation } from '../updateByID.js' -export type BaseOptions = { +export type BaseOptions = { autosave?: boolean collection: TSlug /** @@ -35,40 +42,62 @@ export type BaseOptions = { overwriteExistingFiles?: boolean publishSpecificLocale?: string req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean user?: Document } -export type ByIDOptions = { +export type ByIDOptions< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = { id: number | string limit?: never where?: never -} & BaseOptions +} & BaseOptions -export type ManyOptions = { +export type ManyOptions< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = { id?: never limit?: number where: Where -} & BaseOptions +} & BaseOptions -export type Options = ByIDOptions | ManyOptions +export type Options< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +> = ByIDOptions | ManyOptions -async function updateLocal( +async function updateLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: ByIDOptions, -): Promise> -async function updateLocal( + options: ByIDOptions, +): Promise> +async function updateLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: ManyOptions, -): Promise> -async function updateLocal( + options: ManyOptions, +): Promise> +async function updateLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise | DataFromCollectionSlug> -async function updateLocal( + options: Options, +): Promise | TransformCollectionWithSelect> +async function updateLocal< + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( payload: Payload, - options: Options, -): Promise | DataFromCollectionSlug> { + options: Options, +): Promise | TransformCollectionWithSelect> { const { id, autosave, @@ -84,6 +113,7 @@ async function updateLocal( overrideLock, overwriteExistingFiles = false, publishSpecificLocale, + select, showHiddenFields, where, } = options @@ -114,14 +144,15 @@ async function updateLocal( payload, publishSpecificLocale, req, + select, showHiddenFields, where, } if (options.id) { - return updateByIDOperation(args) + return updateByIDOperation(args) } - return updateOperation(args) + return updateOperation(args) } export default updateLocal diff --git a/packages/payload/src/collections/operations/restoreVersion.ts b/packages/payload/src/collections/operations/restoreVersion.ts index e81a531a5c..d87e7fc38e 100644 --- a/packages/payload/src/collections/operations/restoreVersion.ts +++ b/packages/payload/src/collections/operations/restoreVersion.ts @@ -1,7 +1,7 @@ import httpStatus from 'http-status' import type { FindOneArgs } from '../../database/types.js' -import type { PayloadRequest } from '../../types/index.js' +import type { PayloadRequest, SelectType } from '../../types/index.js' import type { Collection, TypeWithID } from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' @@ -22,6 +22,7 @@ export type Arguments = { id: number | string overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } @@ -36,6 +37,7 @@ export const restoreVersionOperation = async ( overrideAccess = false, req, req: { fallbackLocale, locale, payload }, + select, showHiddenFields, } = args @@ -115,6 +117,7 @@ export const restoreVersionOperation = async ( collection: collectionConfig.slug, data: rawVersion.version, req, + select, }) // ///////////////////////////////////// @@ -150,6 +153,7 @@ export const restoreVersionOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/update.ts b/packages/payload/src/collections/operations/update.ts index ffcba363c2..993fe84ab6 100644 --- a/packages/payload/src/collections/operations/update.ts +++ b/packages/payload/src/collections/operations/update.ts @@ -4,12 +4,13 @@ import httpStatus from 'http-status' import type { AccessResult } from '../../config/types.js' import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest, Where } from '../../types/index.js' +import type { PayloadRequest, SelectType, Where } from '../../types/index.js' import type { BulkOperationResult, Collection, DataFromCollectionSlug, RequiredDataFromCollectionSlug, + SelectFromCollectionSlug, } from '../config/types.js' import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js' @@ -46,13 +47,17 @@ export type Arguments = { overrideLock?: boolean overwriteExistingFiles?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean where: Where } -export const updateOperation = async ( +export const updateOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug, +>( incomingArgs: Arguments, -): Promise> => { +): Promise> => { let args = incomingArgs try { @@ -91,6 +96,7 @@ export const updateOperation = async ( payload, }, req, + select, showHiddenFields, where, } = args @@ -322,6 +328,7 @@ export const updateOperation = async ( data: result, locale, req, + select, }) } @@ -336,6 +343,7 @@ export const updateOperation = async ( docWithLocales: result, payload, req, + select, }) } @@ -354,6 +362,7 @@ export const updateOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/collections/operations/updateByID.ts b/packages/payload/src/collections/operations/updateByID.ts index 6fe0942484..5b4a32c0cf 100644 --- a/packages/payload/src/collections/operations/updateByID.ts +++ b/packages/payload/src/collections/operations/updateByID.ts @@ -5,11 +5,16 @@ import httpStatus from 'http-status' import type { FindOneArgs } from '../../database/types.js' import type { Args } from '../../fields/hooks/beforeChange/index.js' import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' +import type { + PayloadRequest, + SelectType, + TransformCollectionWithSelect, +} from '../../types/index.js' import type { Collection, DataFromCollectionSlug, RequiredDataFromCollectionSlug, + SelectFromCollectionSlug, } from '../config/types.js' import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js' @@ -48,12 +53,16 @@ export type Arguments = { overwriteExistingFiles?: boolean publishSpecificLocale?: string req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } -export const updateByIDOperation = async ( +export const updateByIDOperation = async < + TSlug extends CollectionSlug, + TSelect extends SelectFromCollectionSlug = SelectType, +>( incomingArgs: Arguments, -): Promise> => { +): Promise> => { let args = incomingArgs try { @@ -98,6 +107,7 @@ export const updateByIDOperation = async ( payload, }, req, + select, showHiddenFields, } = args @@ -345,6 +355,7 @@ export const updateByIDOperation = async ( data: dataToUpdate, locale, req, + select, }) } @@ -362,6 +373,7 @@ export const updateByIDOperation = async ( payload, publishSpecificLocale, req, + select, snapshot: versionSnapshotResult, }) } @@ -381,6 +393,7 @@ export const updateByIDOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) @@ -458,7 +471,7 @@ export const updateByIDOperation = async ( await commitTransaction(req) } - return result + return result as TransformCollectionWithSelect } catch (error: unknown) { await killTransaction(args.req) throw error diff --git a/packages/payload/src/collections/operations/utils.ts b/packages/payload/src/collections/operations/utils.ts index c73fd3798c..fc99ed3692 100644 --- a/packages/payload/src/collections/operations/utils.ts +++ b/packages/payload/src/collections/operations/utils.ts @@ -3,7 +3,11 @@ import type { loginOperation } from '../../auth/operations/login.js' import type { refreshOperation } from '../../auth/operations/refresh.js' import type { CollectionSlug } from '../../index.js' import type { PayloadRequest } from '../../types/index.js' -import type { AfterOperationHook, SanitizedCollectionConfig } from '../config/types.js' +import type { + AfterOperationHook, + SanitizedCollectionConfig, + SelectFromCollectionSlug, +} from '../config/types.js' import type { countOperation } from './count.js' import type { createOperation } from './create.js' import type { deleteOperation } from './delete.js' @@ -15,16 +19,26 @@ import type { updateByIDOperation } from './updateByID.js' export type AfterOperationMap = { count: typeof countOperation - create: typeof createOperation - delete: typeof deleteOperation - deleteByID: typeof deleteByIDOperation - find: typeof findOperation - findByID: typeof findByIDOperation + create: typeof createOperation> + delete: typeof deleteOperation> + deleteByID: typeof deleteByIDOperation< + TOperationGeneric, + SelectFromCollectionSlug + > + find: typeof findOperation> + findByID: typeof findByIDOperation< + TOperationGeneric, + boolean, + SelectFromCollectionSlug + > forgotPassword: typeof forgotPasswordOperation login: typeof loginOperation refresh: typeof refreshOperation - update: typeof updateOperation - updateByID: typeof updateByIDOperation + update: typeof updateOperation> + updateByID: typeof updateByIDOperation< + TOperationGeneric, + SelectFromCollectionSlug + > } export type AfterOperationArg = { diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index d56c3dfd66..ee88af26d5 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -1,5 +1,13 @@ import type { TypeWithID } from '../collections/config/types.js' -import type { Document, JoinQuery, Payload, PayloadRequest, Sort, Where } from '../types/index.js' +import type { + Document, + JoinQuery, + Payload, + PayloadRequest, + SelectType, + Sort, + Where, +} from '../types/index.js' import type { TypeWithVersion } from '../versions/types.js' export type { TypeWithVersion } @@ -180,6 +188,7 @@ export type QueryDraftsArgs = { page?: number pagination?: boolean req: PayloadRequest + select?: SelectType sort?: Sort where?: Where } @@ -191,6 +200,7 @@ export type FindOneArgs = { joins?: JoinQuery locale?: string req: PayloadRequest + select?: SelectType where?: Where } @@ -206,6 +216,7 @@ export type FindArgs = { pagination?: boolean projection?: Record req: PayloadRequest + select?: SelectType skip?: number sort?: Sort versions?: boolean @@ -229,6 +240,7 @@ type BaseVersionArgs = { page?: number pagination?: boolean req: PayloadRequest + select?: SelectType skip?: number sort?: Sort versions?: boolean @@ -250,6 +262,7 @@ export type FindGlobalVersionsArgs = { export type FindGlobalArgs = { locale?: string req: PayloadRequest + select?: SelectType slug: string where?: Where } @@ -258,6 +271,7 @@ export type UpdateGlobalVersionArgs = { global: string locale?: string req: PayloadRequest + select?: SelectType versionData: T } & ( | { @@ -290,6 +304,7 @@ export type CreateGlobal = = any>( export type UpdateGlobalArgs = any> = { data: T req: PayloadRequest + select?: SelectType slug: string } export type UpdateGlobal = = any>( @@ -319,6 +334,7 @@ export type CreateVersionArgs = { parent: number | string publishedLocale?: string req: PayloadRequest + select?: SelectType snapshot?: true updatedAt: string versionData: T @@ -336,6 +352,7 @@ export type CreateGlobalVersionArgs = { parent: number | string publishedLocale?: string req: PayloadRequest + select?: SelectType snapshot?: true updatedAt: string versionData: T @@ -351,6 +368,7 @@ export type UpdateVersionArgs = { collection: string locale?: string req: PayloadRequest + select?: SelectType versionData: T } & ( | { @@ -373,6 +391,7 @@ export type CreateArgs = { draft?: boolean locale?: string req: PayloadRequest + select?: SelectType } export type Create = (args: CreateArgs) => Promise @@ -388,6 +407,7 @@ export type UpdateOneArgs = { */ options?: Record req: PayloadRequest + select?: SelectType } & ( | { id: number | string @@ -407,6 +427,7 @@ export type UpsertArgs = { joins?: JoinQuery locale?: string req: PayloadRequest + select?: SelectType where: Where } @@ -416,6 +437,7 @@ export type DeleteOneArgs = { collection: string joins?: JoinQuery req: PayloadRequest + select?: SelectType where: Where } diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 510c6dc98e..90286ee3ae 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -51,6 +51,8 @@ export { export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js' export { getDataByPath } from '../utilities/getDataByPath.js' +export { getSelectMode } from '../utilities/getSelectMode.js' + export { getSiblingData } from '../utilities/getSiblingData.js' export { getUniqueListBy } from '../utilities/getUniqueListBy.js' @@ -70,9 +72,7 @@ export { setsAreEqual } from '../utilities/setsAreEqual.js' export { default as toKebabCase } from '../utilities/toKebabCase.js' export { unflatten } from '../utilities/unflatten.js' - export { wait } from '../utilities/wait.js' - export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js' export { versionDefaults } from '../versions/defaults.js' export { deepMergeSimple } from '@payloadcms/translations/utilities' diff --git a/packages/payload/src/fields/hooks/afterRead/index.ts b/packages/payload/src/fields/hooks/afterRead/index.ts index 1331ee1e14..38f226d042 100644 --- a/packages/payload/src/fields/hooks/afterRead/index.ts +++ b/packages/payload/src/fields/hooks/afterRead/index.ts @@ -1,8 +1,14 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/types.js' import type { SanitizedGlobalConfig } from '../../../globals/config/types.js' -import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { + JsonObject, + PayloadRequest, + RequestContext, + SelectType, +} from '../../../types/index.js' import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js' +import { getSelectMode } from '../../../utilities/getSelectMode.js' import { traverseFields } from './traverseFields.js' type Args = { @@ -19,6 +25,7 @@ type Args = { locale: string overrideAccess: boolean req: PayloadRequest + select?: SelectType showHiddenFields: boolean } @@ -47,6 +54,7 @@ export async function afterRead(args: Args): Promise locale, overrideAccess, req, + select, showHiddenFields, } = args @@ -83,6 +91,8 @@ export async function afterRead(args: Args): Promise populationPromises, req, schemaPath: [], + select, + selectMode: select ? getSelectMode(select) : undefined, showHiddenFields, siblingDoc: doc, }) diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 6091440b4b..e1958ef3df 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -1,7 +1,13 @@ import type { RichTextAdapter } from '../../../admin/RichText.js' import type { SanitizedCollectionConfig } from '../../../collections/config/types.js' import type { SanitizedGlobalConfig } from '../../../globals/config/types.js' -import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { + JsonObject, + PayloadRequest, + RequestContext, + SelectMode, + SelectType, +} from '../../../types/index.js' import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' @@ -39,6 +45,8 @@ type Args = { parentSchemaPath: string[] populationPromises: Promise[] req: PayloadRequest + select?: SelectType + selectMode?: SelectMode showHiddenFields: boolean siblingDoc: JsonObject triggerAccessControl?: boolean @@ -72,6 +80,8 @@ export const promise = async ({ parentSchemaPath, populationPromises, req, + select, + selectMode, showHiddenFields, siblingDoc, triggerAccessControl = true, @@ -92,6 +102,22 @@ export const promise = async ({ delete siblingDoc[field.name] } + if (fieldAffectsData(field) && select && selectMode) { + if (selectMode === 'include') { + if (!select[field.name]) { + delete siblingDoc[field.name] + return + } + } + + if (selectMode === 'exclude') { + if (select[field.name] === false) { + delete siblingDoc[field.name] + return + } + } + } + const shouldHoistLocalizedValue = flattenLocales && fieldAffectsData(field) && @@ -317,6 +343,8 @@ export const promise = async ({ groupDoc = {} } + const groupSelect = select?.[field.name] + traverseFields({ collection, context, @@ -336,6 +364,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select: typeof groupSelect === 'object' ? groupSelect : undefined, + selectMode, showHiddenFields, siblingDoc: groupDoc, triggerAccessControl, @@ -348,6 +378,12 @@ export const promise = async ({ case 'array': { const rows = siblingDoc[field.name] as JsonObject + const arraySelect = select?.[field.name] + + if (selectMode === 'include' && typeof arraySelect === 'object') { + arraySelect.id = true + } + if (Array.isArray(rows)) { rows.forEach((row, i) => { traverseFields({ @@ -369,6 +405,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select: typeof arraySelect === 'object' ? arraySelect : undefined, + selectMode, showHiddenFields, siblingDoc: row || {}, triggerAccessControl, @@ -415,12 +453,38 @@ export const promise = async ({ case 'blocks': { const rows = siblingDoc[field.name] + const blocksSelect = select?.[field.name] + if (Array.isArray(rows)) { rows.forEach((row, i) => { const block = field.blocks.find( (blockType) => blockType.slug === (row as JsonObject).blockType, ) + let blockSelectMode = selectMode + + if (typeof blocksSelect === 'object') { + // sanitize blocks: {cta: false} to blocks: {cta: {id: true, blockType: true}} + if (selectMode === 'exclude' && blocksSelect[block.slug] === false) { + blockSelectMode = 'include' + blocksSelect[block.slug] = { + id: true, + blockType: true, + } + } else if (selectMode === 'include') { + if (!blocksSelect[block.slug]) { + blocksSelect[block.slug] = {} + } + + if (typeof blocksSelect[block.slug] === 'object') { + blocksSelect[block.slug]['id'] = true + blocksSelect[block.slug]['blockType'] = true + } + } + } + + const blockSelect = blocksSelect?.[block.slug] + if (block) { traverseFields({ collection, @@ -441,6 +505,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select: typeof blockSelect === 'object' ? blockSelect : undefined, + selectMode: blockSelectMode, showHiddenFields, siblingDoc: (row as JsonObject) || {}, triggerAccessControl, @@ -513,6 +579,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select, + selectMode, showHiddenFields, siblingDoc, triggerAccessControl, @@ -524,11 +592,18 @@ export const promise = async ({ case 'tab': { let tabDoc = siblingDoc + let tabSelect: SelectType | undefined if (tabHasName(field)) { tabDoc = siblingDoc[field.name] as JsonObject if (typeof siblingDoc[field.name] !== 'object') { tabDoc = {} } + + if (typeof select?.[field.name] === 'object') { + tabSelect = select?.[field.name] as SelectType + } + } else { + tabSelect = select } traverseFields({ @@ -550,6 +625,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select: tabSelect, + selectMode, showHiddenFields, siblingDoc: tabDoc, triggerAccessControl, @@ -579,6 +656,8 @@ export const promise = async ({ populationPromises, req, schemaPath: fieldSchemaPath, + select, + selectMode, showHiddenFields, siblingDoc, triggerAccessControl, diff --git a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts index 0f36028ef5..2293299099 100644 --- a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts @@ -1,6 +1,12 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/types.js' import type { SanitizedGlobalConfig } from '../../../globals/config/types.js' -import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js' +import type { + JsonObject, + PayloadRequest, + RequestContext, + SelectMode, + SelectType, +} from '../../../types/index.js' import type { Field, TabAsField } from '../../config/types.js' import { promise } from './promise.js' @@ -27,6 +33,8 @@ type Args = { populationPromises: Promise[] req: PayloadRequest schemaPath: string[] + select?: SelectType + selectMode?: SelectMode showHiddenFields: boolean siblingDoc: JsonObject triggerAccessControl?: boolean @@ -52,6 +60,8 @@ export const traverseFields = ({ populationPromises, req, schemaPath, + select, + selectMode, showHiddenFields, siblingDoc, triggerAccessControl = true, @@ -78,6 +88,8 @@ export const traverseFields = ({ parentSchemaPath: schemaPath, populationPromises, req, + select, + selectMode, showHiddenFields, siblingDoc, triggerAccessControl, diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index f26ee50d39..3720d414cc 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -19,12 +19,14 @@ import type { } from '../../config/types.js' import type { DBIdentifierName } from '../../database/types.js' import type { Field } from '../../fields/config/types.js' -import type { GlobalSlug, TypedGlobal } from '../../index.js' +import type { GlobalSlug, TypedGlobal, TypedGlobalSelect } from '../../index.js' import type { PayloadRequest, RequestContext, Where } from '../../types/index.js' import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types.js' export type DataFromGlobalSlug = TypedGlobal[TSlug] +export type SelectFromGlobalSlug = TypedGlobalSelect[TSlug] + export type BeforeValidateHook = (args: { context: RequestContext data?: any diff --git a/packages/payload/src/globals/operations/findOne.ts b/packages/payload/src/globals/operations/findOne.ts index 70989a2850..83290517a7 100644 --- a/packages/payload/src/globals/operations/findOne.ts +++ b/packages/payload/src/globals/operations/findOne.ts @@ -1,9 +1,10 @@ import type { AccessResult } from '../../config/types.js' -import type { PayloadRequest, Where } from '../../types/index.js' +import type { PayloadRequest, SelectType, Where } from '../../types/index.js' import type { SanitizedGlobalConfig } from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { afterRead } from '../../fields/hooks/afterRead/index.js' +import { getSelectMode } from '../../utilities/getSelectMode.js' import { killTransaction } from '../../utilities/killTransaction.js' import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable.js' @@ -14,6 +15,7 @@ type Args = { includeLockStatus?: boolean overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean slug: string } @@ -30,6 +32,7 @@ export const findOneOperation = async >( overrideAccess = false, req: { fallbackLocale, locale }, req, + select, showHiddenFields, } = args @@ -52,6 +55,7 @@ export const findOneOperation = async >( slug, locale, req, + select, where: overrideAccess ? undefined : (accessResult as Where), }) if (!doc) { @@ -103,6 +107,7 @@ export const findOneOperation = async >( entityType: 'global', overrideAccess, req, + select, }) } @@ -122,6 +127,19 @@ export const findOneOperation = async >( })) || doc }, Promise.resolve()) + // ///////////////////////////////////// + // Execute globalType field if not selected + // ///////////////////////////////////// + if (select && doc.globalType) { + const selectMode = getSelectMode(select) + if ( + (selectMode === 'include' && !select['globalType']) || + (selectMode === 'exclude' && select['globalType'] === false) + ) { + delete doc['globalType'] + } + } + // ///////////////////////////////////// // Execute field-level hooks and access // ///////////////////////////////////// @@ -137,6 +155,7 @@ export const findOneOperation = async >( locale, overrideAccess, req, + select, showHiddenFields, }) diff --git a/packages/payload/src/globals/operations/findVersionByID.ts b/packages/payload/src/globals/operations/findVersionByID.ts index 7e1f08aeaa..31dedd829e 100644 --- a/packages/payload/src/globals/operations/findVersionByID.ts +++ b/packages/payload/src/globals/operations/findVersionByID.ts @@ -1,5 +1,5 @@ import type { FindGlobalVersionsArgs } from '../../database/types.js' -import type { PayloadRequest } from '../../types/index.js' +import type { PayloadRequest, SelectType } from '../../types/index.js' import type { TypeWithVersion } from '../../versions/types.js' import type { SanitizedGlobalConfig } from '../config/types.js' @@ -8,6 +8,7 @@ import { combineQueries } from '../../database/combineQueries.js' import { Forbidden, NotFound } from '../../errors/index.js' import { afterRead } from '../../fields/hooks/afterRead/index.js' import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js' +import { getSelectMode } from '../../utilities/getSelectMode.js' import { killTransaction } from '../../utilities/killTransaction.js' export type Arguments = { @@ -18,6 +19,7 @@ export type Arguments = { id: number | string overrideAccess?: boolean req: PayloadRequest + select?: SelectType showHiddenFields?: boolean } @@ -33,6 +35,7 @@ export const findVersionByIDOperation = async = an overrideAccess, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, } = args @@ -57,6 +60,7 @@ export const findVersionByIDOperation = async = an limit: 1, locale, req, + select, where: combineQueries({ id: { equals: id } }, accessResults), } @@ -120,6 +124,7 @@ export const findVersionByIDOperation = async = an locale, overrideAccess, req, + select: typeof select?.version === 'object' ? select.version : undefined, showHiddenFields, }) diff --git a/packages/payload/src/globals/operations/findVersions.ts b/packages/payload/src/globals/operations/findVersions.ts index 33383901a9..aa2018cc62 100644 --- a/packages/payload/src/globals/operations/findVersions.ts +++ b/packages/payload/src/globals/operations/findVersions.ts @@ -1,5 +1,5 @@ import type { PaginatedDocs } from '../../database/types.js' -import type { PayloadRequest, Sort, Where } from '../../types/index.js' +import type { PayloadRequest, SelectType, Sort, Where } from '../../types/index.js' import type { TypeWithVersion } from '../../versions/types.js' import type { SanitizedGlobalConfig } from '../config/types.js' @@ -19,6 +19,7 @@ export type Arguments = { page?: number pagination?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean sort?: Sort where?: Where @@ -36,6 +37,7 @@ export const findVersionsOperation = async >( pagination = true, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, sort, where, @@ -73,6 +75,7 @@ export const findVersionsOperation = async >( page: page || 1, pagination, req, + select, sort, where: fullWhere, }) @@ -84,27 +87,30 @@ export const findVersionsOperation = async >( let result = { ...paginatedDocs, docs: await Promise.all( - paginatedDocs.docs.map(async (data) => ({ - ...data, - version: await afterRead({ - collection: null, - context: req.context, - depth, - doc: { - ...data.version, - // Patch globalType onto version doc - globalType: globalConfig.slug, - }, - draft: undefined, - fallbackLocale, - findMany: true, - global: globalConfig, - locale, - overrideAccess, - req, - showHiddenFields, - }), - })), + paginatedDocs.docs.map(async (data) => { + return { + ...data, + version: await afterRead({ + collection: null, + context: req.context, + depth, + doc: { + ...data.version, + // Patch globalType onto version doc + globalType: globalConfig.slug, + }, + draft: undefined, + fallbackLocale, + findMany: true, + global: globalConfig, + locale, + overrideAccess, + req, + select, + showHiddenFields, + }), + } + }), ), } as PaginatedDocs diff --git a/packages/payload/src/globals/operations/local/findOne.ts b/packages/payload/src/globals/operations/local/findOne.ts index 8d68a8e163..1538de6063 100644 --- a/packages/payload/src/globals/operations/local/findOne.ts +++ b/packages/payload/src/globals/operations/local/findOne.ts @@ -1,12 +1,17 @@ import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest } from '../../../types/index.js' -import type { DataFromGlobalSlug } from '../../config/types.js' +import type { + Document, + PayloadRequest, + SelectType, + TransformGlobalWithSelect, +} from '../../../types/index.js' +import type { SelectFromGlobalSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { findOneOperation } from '../findOne.js' -export type Options = { +export type Options = { context?: RequestContext depth?: number draft?: boolean @@ -15,21 +20,26 @@ export type Options = { locale?: 'all' | TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean slug: TSlug user?: Document } -export default async function findOneLocal( +export default async function findOneLocal< + TSlug extends GlobalSlug, + TSelect extends SelectFromGlobalSlug, +>( payload: Payload, - options: Options, -): Promise> { + options: Options, +): Promise> { const { slug: globalSlug, depth, draft = false, includeLockStatus, overrideAccess = true, + select, showHiddenFields, } = options @@ -47,6 +57,7 @@ export default async function findOneLocal( includeLockStatus, overrideAccess, req: await createLocalReq(options, payload), + select, showHiddenFields, }) } diff --git a/packages/payload/src/globals/operations/local/findVersionByID.ts b/packages/payload/src/globals/operations/local/findVersionByID.ts index 8bae4aa766..236b5188a2 100644 --- a/packages/payload/src/globals/operations/local/findVersionByID.ts +++ b/packages/payload/src/globals/operations/local/findVersionByID.ts @@ -1,5 +1,5 @@ import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest } from '../../../types/index.js' +import type { Document, PayloadRequest, SelectType } from '../../../types/index.js' import type { TypeWithVersion } from '../../../versions/types.js' import type { DataFromGlobalSlug } from '../../config/types.js' @@ -16,6 +16,7 @@ export type Options = { locale?: 'all' | TypedLocale overrideAccess?: boolean req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean slug: TSlug user?: Document @@ -32,6 +33,7 @@ export default async function findVersionByIDLocal( depth, disableErrors = false, overrideAccess = true, + select, showHiddenFields, } = options @@ -48,6 +50,7 @@ export default async function findVersionByIDLocal( globalConfig, overrideAccess, req: await createLocalReq(options, payload), + select, showHiddenFields, }) } diff --git a/packages/payload/src/globals/operations/local/findVersions.ts b/packages/payload/src/globals/operations/local/findVersions.ts index 67a4e1a1e4..61dd15072e 100644 --- a/packages/payload/src/globals/operations/local/findVersions.ts +++ b/packages/payload/src/globals/operations/local/findVersions.ts @@ -1,6 +1,7 @@ +/* eslint-disable no-restricted-exports */ import type { PaginatedDocs } from '../../../database/types.js' import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest, Sort, Where } from '../../../types/index.js' +import type { Document, PayloadRequest, SelectType, Sort, Where } from '../../../types/index.js' import type { TypeWithVersion } from '../../../versions/types.js' import type { DataFromGlobalSlug } from '../../config/types.js' @@ -17,6 +18,7 @@ export type Options = { overrideAccess?: boolean page?: number req?: PayloadRequest + select?: SelectType showHiddenFields?: boolean slug: TSlug sort?: Sort @@ -34,6 +36,7 @@ export default async function findVersionsLocal( limit, overrideAccess = true, page, + select, showHiddenFields, sort, where, @@ -52,6 +55,7 @@ export default async function findVersionsLocal( overrideAccess, page, req: await createLocalReq(options, payload), + select, showHiddenFields, sort, where, diff --git a/packages/payload/src/globals/operations/local/restoreVersion.ts b/packages/payload/src/globals/operations/local/restoreVersion.ts index 48a8c8f29f..f9e683722f 100644 --- a/packages/payload/src/globals/operations/local/restoreVersion.ts +++ b/packages/payload/src/globals/operations/local/restoreVersion.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-exports */ import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js' import type { Document, PayloadRequest } from '../../../types/index.js' import type { DataFromGlobalSlug } from '../../config/types.js' diff --git a/packages/payload/src/globals/operations/local/update.ts b/packages/payload/src/globals/operations/local/update.ts index 1a00e3edd3..7b25843a17 100644 --- a/packages/payload/src/globals/operations/local/update.ts +++ b/packages/payload/src/globals/operations/local/update.ts @@ -1,14 +1,19 @@ import type { DeepPartial } from 'ts-essentials' import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js' -import type { Document, PayloadRequest } from '../../../types/index.js' -import type { DataFromGlobalSlug } from '../../config/types.js' +import type { + Document, + PayloadRequest, + SelectType, + TransformGlobalWithSelect, +} from '../../../types/index.js' +import type { DataFromGlobalSlug, SelectFromGlobalSlug } from '../../config/types.js' import { APIError } from '../../../errors/index.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { updateOperation } from '../update.js' -export type Options = { +export type Options = { context?: RequestContext data: DeepPartial, 'id'>> depth?: number @@ -19,15 +24,19 @@ export type Options = { overrideLock?: boolean publishSpecificLocale?: TypedLocale req?: PayloadRequest + select?: TSelect showHiddenFields?: boolean slug: TSlug user?: Document } -export default async function updateLocal( +export default async function updateLocal< + TSlug extends GlobalSlug, + TSelect extends SelectFromGlobalSlug, +>( payload: Payload, - options: Options, -): Promise> { + options: Options, +): Promise> { const { slug: globalSlug, data, @@ -36,6 +45,7 @@ export default async function updateLocal( overrideAccess = true, overrideLock, publishSpecificLocale, + select, showHiddenFields, } = options @@ -45,7 +55,7 @@ export default async function updateLocal( throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`) } - return updateOperation({ + return updateOperation({ slug: globalSlug as string, data, depth, @@ -55,6 +65,7 @@ export default async function updateLocal( overrideLock, publishSpecificLocale, req: await createLocalReq(options, payload), + select, showHiddenFields, }) } diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index d6d01c8880..e59e21eb5f 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -1,8 +1,18 @@ import type { DeepPartial } from 'ts-essentials' import type { GlobalSlug, JsonObject } from '../../index.js' -import type { Operation, PayloadRequest, Where } from '../../types/index.js' -import type { DataFromGlobalSlug, SanitizedGlobalConfig } from '../config/types.js' +import type { + Operation, + PayloadRequest, + SelectType, + TransformGlobalWithSelect, + Where, +} from '../../types/index.js' +import type { + DataFromGlobalSlug, + SanitizedGlobalConfig, + SelectFromGlobalSlug, +} from '../config/types.js' import executeAccess from '../../auth/executeAccess.js' import { afterChange } from '../../fields/hooks/afterChange/index.js' @@ -12,6 +22,7 @@ import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js' import { deepCopyObjectSimple } from '../../index.js' import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js' import { commitTransaction } from '../../utilities/commitTransaction.js' +import { getSelectMode } from '../../utilities/getSelectMode.js' import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { getLatestGlobalVersion } from '../../versions/getLatestGlobalVersion.js' @@ -28,13 +39,17 @@ type Args = { overrideLock?: boolean publishSpecificLocale?: string req: PayloadRequest + select?: SelectType showHiddenFields?: boolean slug: string } -export const updateOperation = async ( +export const updateOperation = async < + TSlug extends GlobalSlug, + TSelect extends SelectFromGlobalSlug, +>( args: Args, -): Promise> => { +): Promise> => { if (args.publishSpecificLocale) { args.req.locale = args.publishSpecificLocale } @@ -51,6 +66,7 @@ export const updateOperation = async ( publishSpecificLocale, req: { fallbackLocale, locale, payload }, req, + select, showHiddenFields, } = args @@ -230,6 +246,7 @@ export const updateOperation = async ( slug, data: result, req, + select, }) } else { result = await payload.db.createGlobal({ @@ -253,6 +270,7 @@ export const updateOperation = async ( payload, publishSpecificLocale, req, + select, snapshot: versionSnapshotResult, }) @@ -262,6 +280,19 @@ export const updateOperation = async ( } } + // ///////////////////////////////////// + // Execute globalType field if not selected + // ///////////////////////////////////// + if (select && result.globalType) { + const selectMode = getSelectMode(select) + if ( + (selectMode === 'include' && !select['globalType']) || + (selectMode === 'exclude' && select['globalType'] === false) + ) { + delete result['globalType'] + } + } + // ///////////////////////////////////// // afterRead - Fields // ///////////////////////////////////// @@ -277,6 +308,7 @@ export const updateOperation = async ( locale, overrideAccess, req, + select, showHiddenFields, }) @@ -336,7 +368,7 @@ export const updateOperation = async ( await commitTransaction(req) } - return result + return result as TransformGlobalWithSelect } catch (error: unknown) { await killTransaction(req) throw error diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index ece0e1de7e..4e3281404d 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -22,6 +22,7 @@ import type { BulkOperationResult, Collection, DataFromCollectionSlug, + SelectFromCollectionSlug, TypeWithID, } from './collections/config/types.js' export type * from './admin/types.js' @@ -33,6 +34,7 @@ import type { Options as DeleteOptions, } from './collections/operations/local/delete.js' export type { MappedView } from './admin/views/types.js' + import type { Options as DuplicateOptions } from './collections/operations/local/duplicate.js' import type { Options as FindOptions } from './collections/operations/local/find.js' import type { Options as FindByIDOptions } from './collections/operations/local/findByID.js' @@ -47,13 +49,19 @@ import type { import type { InitOptions, SanitizedConfig } from './config/types.js' import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js' import type { InitializedEmailAdapter } from './email/types.js' -import type { DataFromGlobalSlug, Globals } from './globals/config/types.js' +import type { DataFromGlobalSlug, Globals, SelectFromGlobalSlug } from './globals/config/types.js' import type { Options as FindGlobalOptions } from './globals/operations/local/findOne.js' import type { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID.js' import type { Options as FindGlobalVersionsOptions } from './globals/operations/local/findVersions.js' import type { Options as RestoreGlobalVersionOptions } from './globals/operations/local/restoreVersion.js' import type { Options as UpdateGlobalOptions } from './globals/operations/local/update.js' -import type { JsonObject } from './types/index.js' +import type { + ApplyDisableErrors, + JsonObject, + SelectType, + TransformCollectionWithSelect, + TransformGlobalWithSelect, +} from './types/index.js' import type { TraverseFieldsCallback } from './utilities/traverseFields.js' import type { TypeWithVersion } from './versions/types.js' @@ -88,12 +96,20 @@ export interface GeneratedTypes { } } } + collectionsSelectUntyped: { + [slug: string]: SelectType + } + collectionsUntyped: { [slug: string]: JsonObject & TypeWithID } dbUntyped: { defaultIDType: number | string } + globalsSelectUntyped: { + [slug: string]: SelectType + } + globalsUntyped: { [slug: string]: JsonObject } @@ -106,13 +122,29 @@ type ResolveCollectionType = 'collections' extends keyof T ? T['collections'] : // @ts-expect-error T['collectionsUntyped'] -// @ts-expect-error -type ResolveGlobalType = 'globals' extends keyof T ? T['globals'] : T['globalsUntyped'] + +type ResolveCollectionSelectType = 'collectionsSelect' extends keyof T + ? T['collectionsSelect'] + : // @ts-expect-error + T['collectionsSelectUntyped'] +type ResolveGlobalType = 'globals' extends keyof T + ? T['globals'] + : // @ts-expect-error + T['globalsUntyped'] + +type ResolveGlobalSelectType = 'globalsSelect' extends keyof T + ? T['globalsSelect'] + : // @ts-expect-error + T['globalsSelectUntyped'] // Applying helper types to GeneratedTypes export type TypedCollection = ResolveCollectionType + +export type TypedCollectionSelect = ResolveCollectionSelectType export type TypedGlobal = ResolveGlobalType +export type TypedGlobalSelect = ResolveGlobalSelectType + // Extract string keys from the type type StringKeyOf = Extract @@ -184,21 +216,21 @@ export class BasePayload { * @param options * @returns created document */ - create = async ( - options: CreateOptions, - ): Promise> => { + create = async >( + options: CreateOptions, + ): Promise> => { const { create } = localOperations - return create(this, options) + return create(this, options) } db: DatabaseAdapter decrypt = decrypt - duplicate = async ( - options: DuplicateOptions, - ): Promise> => { + duplicate = async >( + options: DuplicateOptions, + ): Promise> => { const { duplicate } = localOperations - return duplicate(this, options) + return duplicate(this, options) } email: InitializedEmailAdapter @@ -219,11 +251,11 @@ export class BasePayload { * @param options * @returns documents satisfying query */ - find = async ( - options: FindOptions, - ): Promise>> => { + find = async >( + options: FindOptions, + ): Promise>> => { const { find } = localOperations - return find(this, options) + return find(this, options) } /** @@ -231,22 +263,22 @@ export class BasePayload { * @param options * @returns document with specified ID */ - findByID = async ( - options: TOptions, - ): Promise< - TOptions['disableErrors'] extends true - ? DataFromCollectionSlug | null - : DataFromCollectionSlug - > => { + findByID = async < + TSlug extends CollectionSlug, + TDisableErrors extends boolean, + TSelect extends SelectFromCollectionSlug, + >( + options: FindByIDOptions, + ): Promise, TDisableErrors>> => { const { findByID } = localOperations - return findByID(this, options) + return findByID(this, options) } - findGlobal = async ( - options: FindGlobalOptions, - ): Promise> => { + findGlobal = async >( + options: FindGlobalOptions, + ): Promise> => { const { findOne } = localGlobalOperations - return findOne(this, options) + return findOne(this, options) } /** @@ -375,11 +407,11 @@ export class BasePayload { return unlock(this, options) } - updateGlobal = async ( - options: UpdateGlobalOptions, - ): Promise> => { + updateGlobal = async >( + options: UpdateGlobalOptions, + ): Promise> => { const { update } = localGlobalOperations - return update(this, options) + return update(this, options) } validationRules: (args: OperationArgs) => ValidationRule[] @@ -425,19 +457,19 @@ export class BasePayload { * @param options * @returns Updated document(s) */ - delete( - options: DeleteByIDOptions, - ): Promise> + delete>( + options: DeleteByIDOptions, + ): Promise> - delete( - options: DeleteManyOptions, - ): Promise> + delete>( + options: DeleteManyOptions, + ): Promise> - delete( - options: DeleteOptions, - ): Promise | DataFromCollectionSlug> { + delete>( + options: DeleteOptions, + ): Promise | TransformCollectionWithSelect> { const { deleteLocal } = localOperations - return deleteLocal(this, options) + return deleteLocal(this, options) } /** @@ -593,24 +625,24 @@ export class BasePayload { return this } - update( - options: UpdateManyOptions, - ): Promise> + update>( + options: UpdateManyOptions, + ): Promise> /** * @description Update one or more documents * @param options * @returns Updated document(s) */ - update( - options: UpdateByIDOptions, - ): Promise> + update>( + options: UpdateByIDOptions, + ): Promise> - update( - options: UpdateOptions, - ): Promise | DataFromCollectionSlug> { + update>( + options: UpdateOptions, + ): Promise | TransformCollectionWithSelect> { const { update } = localOperations - return update(this, options) + return update(this, options) } } diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index 7890e9e944..192aa06eeb 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -2,9 +2,19 @@ import type { I18n, TFunction } from '@payloadcms/translations' import type DataLoader from 'dataloader' import type { URL } from 'url' -import type { TypeWithID, TypeWithTimestamps } from '../collections/config/types.js' +import type { + DataFromCollectionSlug, + TypeWithID, + TypeWithTimestamps, +} from '../collections/config/types.js' import type payload from '../index.js' -import type { TypedLocale, TypedUser } from '../index.js' +import type { + CollectionSlug, + DataFromGlobalSlug, + GlobalSlug, + TypedLocale, + TypedUser, +} from '../index.js' import type { validOperators } from './constants.js' export type { Payload as Payload } from '../index.js' @@ -140,3 +150,73 @@ export function docHasTimestamps(doc: any): doc is TypeWithTimestamps { export type IfAny = 0 extends 1 & T ? Y : N // This is a commonly used trick to detect 'any' export type IsAny = IfAny export type ReplaceAny = IsAny extends true ? DefaultType : T + +export type SelectIncludeType = { + [k: string]: SelectIncludeType | true +} + +export type SelectExcludeType = { + [k: string]: false | SelectExcludeType +} + +export type SelectMode = 'exclude' | 'include' + +export type SelectType = SelectExcludeType | SelectIncludeType + +export type ApplyDisableErrors = DisableErrors extends true + ? null | T + : T + +export type TransformDataWithSelect< + Data extends Record, + Select extends SelectType, +> = Select extends never + ? Data + : string extends keyof Select + ? Data + : // START Handle types when they aren't generated + // For example in any package in this repository outside of tests / plugins + // This stil gives us autocomplete when using include select mode, i.e select: {title :true} returns type {title: any, id: string | number} + string extends keyof Omit + ? Select extends SelectIncludeType + ? { + [K in Data extends TypeWithID ? 'id' | keyof Select : keyof Select]: K extends 'id' + ? number | string + : unknown + } + : Data + : // END Handle types when they aren't generated + // Handle include mode + Select extends SelectIncludeType + ? { + [K in keyof Data as K extends keyof Select + ? Select[K] extends object | true + ? K + : never + : // select 'id' always + K extends 'id' + ? K + : never]: Data[K] + } + : // Handle exclude mode + { + [K in keyof Data as K extends keyof Select + ? Select[K] extends object | undefined + ? K + : never + : K]: Data[K] + } + +export type TransformCollectionWithSelect< + TSlug extends CollectionSlug, + TSelect extends SelectType, +> = TSelect extends SelectType + ? TransformDataWithSelect, TSelect> + : DataFromCollectionSlug + +export type TransformGlobalWithSelect< + TSlug extends GlobalSlug, + TSelect extends SelectType, +> = TSelect extends SelectType + ? TransformDataWithSelect, TSelect> + : DataFromGlobalSlug diff --git a/packages/payload/src/utilities/addSelectGenericsToGeneratedTyoes.spec.ts b/packages/payload/src/utilities/addSelectGenericsToGeneratedTyoes.spec.ts new file mode 100644 index 0000000000..0ba7316b14 --- /dev/null +++ b/packages/payload/src/utilities/addSelectGenericsToGeneratedTyoes.spec.ts @@ -0,0 +1,564 @@ +import { addSelectGenericsToGeneratedTypes } from './addSelectGenericsToGeneretedTypes.js' + +const INPUT_AND_OUTPUT = [ + { + input: ` +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run \`payload generate:types\` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + posts: Post; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsSelect?: { + posts: PostsSelect; + users: UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: {}; + globalsSelect?: {}; + locale: null; + user: User & { + collection: 'users'; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "posts". + */ +export interface Post { + id: string; + text?: string | null; + number?: number | null; + group?: { + text?: string | null; + number?: number | null; + }; + array?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + blocks?: + | ( + | { + text?: string | null; + introText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'intro'; + } + | { + text?: string | null; + ctaText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'cta'; + } + )[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: + | ({ + relationTo: 'posts'; + value: string | Post; + } | null) + | ({ + relationTo: 'users'; + value: string | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "posts_select". + */ +export interface PostsSelect { + text?: boolean; + number?: boolean; + group?: + | boolean + | { + text?: boolean; + number?: boolean; + }; + array?: + | boolean + | { + text?: boolean; + number?: boolean; + id?: boolean; + }; + blocks?: + | boolean + | { + intro?: + | boolean + | { + text?: boolean; + introText?: boolean; + id?: boolean; + blockName?: boolean; + }; + cta?: + | boolean + | { + text?: boolean; + ctaText?: boolean; + id?: boolean; + blockName?: boolean; + }; + }; + updatedAt?: boolean; + createdAt?: boolean; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "users_select". + */ +export interface UsersSelect { + updatedAt?: boolean; + createdAt?: boolean; + email?: boolean; + resetPasswordToken?: boolean; + resetPasswordExpiration?: boolean; + salt?: boolean; + hash?: boolean; + loginAttempts?: boolean; + lockUntil?: boolean; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: boolean; + globalSlug?: boolean; + user?: boolean; + updatedAt?: boolean; + createdAt?: boolean; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: boolean; + key?: boolean; + value?: boolean; + updatedAt?: boolean; + createdAt?: boolean; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: boolean; + batch?: boolean; + updatedAt?: boolean; + createdAt?: boolean; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} +`, + output: ` +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run \`payload generate:types\` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + posts: Post; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsSelect?: { + posts: PostsSelect | PostsSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: {}; + globalsSelect?: {}; + locale: null; + user: User & { + collection: 'users'; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "posts". + */ +export interface Post { + id: string; + text?: string | null; + number?: number | null; + group?: { + text?: string | null; + number?: number | null; + }; + array?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + blocks?: + | ( + | { + text?: string | null; + introText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'intro'; + } + | { + text?: string | null; + ctaText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'cta'; + } + )[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: + | ({ + relationTo: 'posts'; + value: string | Post; + } | null) + | ({ + relationTo: 'users'; + value: string | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "posts_select". + */ +export interface PostsSelect { + text?: T; + number?: T; + group?: + | T + | { + text?: T; + number?: T; + }; + array?: + | T + | { + text?: T; + number?: T; + id?: T; + }; + blocks?: + | T + | { + intro?: + | T + | { + text?: T; + introText?: T; + id?: T; + blockName?: T; + }; + cta?: + | T + | { + text?: T; + ctaText?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by \`Config\`'s JSON-Schema + * via the \`definition\` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} +`, + }, +] + +describe('addSelectGenericsToGeneratedTypes', () => { + it('should match return of given input with output', () => { + for (const { input, output } of INPUT_AND_OUTPUT) { + expect( + addSelectGenericsToGeneratedTypes({ + compiledGeneratedTypes: input, + }), + ).toStrictEqual(output) + } + }) +}) diff --git a/packages/payload/src/utilities/addSelectGenericsToGeneretedTypes.ts b/packages/payload/src/utilities/addSelectGenericsToGeneretedTypes.ts new file mode 100644 index 0000000000..c45209c96d --- /dev/null +++ b/packages/payload/src/utilities/addSelectGenericsToGeneretedTypes.ts @@ -0,0 +1,51 @@ +export const addSelectGenericsToGeneratedTypes = ({ + compiledGeneratedTypes, +}: { + compiledGeneratedTypes: string +}) => { + const modifiedLines = [] + + let isCollectionsSelectToken = false + let isSelectTypeToken = false + + for (const line of compiledGeneratedTypes.split('\n')) { + let newLine = line + if (line === ` collectionsSelect?: {` || line === ` globalsSelect?: {`) { + isCollectionsSelectToken = true + } + + if (isCollectionsSelectToken) { + if (line === ' };') { + isCollectionsSelectToken = false + } else { + // replace with | PostsSelect + newLine = line.replace(/(['"]?\w+['"]?):\s*(\w+);/g, (_, variable, type) => { + return `${variable}: ${type} | ${type};` + }) + } + } + + // eslint-disable-next-line regexp/no-unused-capturing-group + if (line.match(/via the `definition` "([\w-]+_select)"/g)) { + isSelectTypeToken = true + } + + if (isSelectTypeToken) { + if (line.startsWith('export interface')) { + // add generic to the interface + newLine = line.replace(/(export interface\s+\w+)(\s*\{)/g, '$1$2') + } else { + // replace booleans with T on the line + newLine = line.replace(/(? { + acc[slug] = { + $ref: `#/definitions/${slug}_select`, + } + + return acc + }, {}) + + return { + type: 'object', + additionalProperties: false, + properties, + required: Object.keys(properties), + } +} + function generateLocaleEntitySchemas(localization: SanitizedConfig['localization']): JSONSchema4 { if (localization && 'locales' in localization && localization?.locales) { const localesFromConfig = localization?.locales @@ -633,6 +652,98 @@ export function entityToJSONSchema( } } +export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONSchema4 { + const schema: JSONSchema4 = { + type: 'object', + additionalProperties: false, + properties: {}, + } + + for (const field of fields) { + switch (field.type) { + case 'row': + case 'collapsible': + schema.properties = { + ...schema.properties, + ...fieldsToSelectJSONSchema({ fields: field.fields }).properties, + } + + break + + case 'array': + case 'group': + schema.properties[field.name] = { + oneOf: [ + { + type: 'boolean', + }, + fieldsToSelectJSONSchema({ fields: field.fields }), + ], + } + break + + case 'tabs': + for (const tab of field.tabs) { + if (tabHasName(tab)) { + schema.properties[tab.name] = { + oneOf: [ + { + type: 'boolean', + }, + fieldsToSelectJSONSchema({ fields: tab.fields }), + ], + } + continue + } + + schema.properties = { + ...schema.properties, + ...fieldsToSelectJSONSchema({ fields: tab.fields }).properties, + } + } + break + + case 'blocks': { + const blocksSchema: JSONSchema4 = { + type: 'object', + additionalProperties: false, + properties: {}, + } + + for (const block of field.blocks) { + blocksSchema.properties[block.slug] = { + oneOf: [ + { + type: 'boolean', + }, + fieldsToSelectJSONSchema({ fields: block.fields }), + ], + } + } + + schema.properties[field.name] = { + oneOf: [ + { + type: 'boolean', + }, + blocksSchema, + ], + } + + break + } + + default: + schema.properties[field.name] = { + type: 'boolean', + } + break + } + } + + return schema +} + const fieldType: JSONSchema4 = { type: 'string', required: false, @@ -803,13 +914,39 @@ export function configToJSONSchema( // Collections and Globals have to be moved to the top-level definitions as well. Reason: The top-level type will be the `Config` type - we don't want all collection and global // types to be inlined inside the `Config` type - const entityDefinitions: { [k: string]: JSONSchema4 } = [ - ...config.globals, - ...config.collections, - ].reduce((acc, entity) => { - acc[entity.slug] = entityToJSONSchema(config, entity, interfaceNameDefinitions, defaultIDType) - return acc - }, {}) + + const entities: { + entity: SanitizedCollectionConfig | SanitizedGlobalConfig + type: 'collection' | 'global' + }[] = [ + ...config.globals.map((global) => ({ type: 'global' as const, entity: global })), + ...config.collections.map((collection) => ({ + type: 'collection' as const, + entity: collection, + })), + ] + + const entityDefinitions: { [k: string]: JSONSchema4 } = entities.reduce( + (acc, { type, entity }) => { + acc[entity.slug] = entityToJSONSchema(config, entity, interfaceNameDefinitions, defaultIDType) + const select = fieldsToSelectJSONSchema({ fields: entity.fields }) + + if (type === 'global') { + select.properties.globalType = { + type: 'boolean', + } + } + + acc[`${entity.slug}_select`] = { + type: 'object', + additionalProperties: false, + ...select, + } + + return acc + }, + {}, + ) const authOperationDefinitions = [...config.collections] .filter(({ auth }) => Boolean(auth)) @@ -833,8 +970,10 @@ export function configToJSONSchema( properties: { auth: generateAuthOperationSchemas(config.collections), collections: generateEntitySchemas(config.collections || []), + collectionsSelect: generateEntitySelectSchemas(config.collections || []), db: generateDbEntitySchema(config), globals: generateEntitySchemas(config.globals || []), + globalsSelect: generateEntitySelectSchemas(config.globals || []), locale: generateLocaleEntitySchemas(config.localization), user: generateAuthEntitySchemas(config.collections), }, diff --git a/packages/payload/src/utilities/getSelectMode.ts b/packages/payload/src/utilities/getSelectMode.ts new file mode 100644 index 0000000000..a930e12534 --- /dev/null +++ b/packages/payload/src/utilities/getSelectMode.ts @@ -0,0 +1,16 @@ +import type { SelectMode, SelectType } from '../types/index.js' + +export const getSelectMode = (select: SelectType): SelectMode => { + for (const key in select) { + const selectValue = select[key] + if (selectValue === false) { + return 'exclude' + } + + if (typeof selectValue === 'object') { + return getSelectMode(selectValue) + } + } + + return 'include' +} diff --git a/packages/payload/src/versions/drafts/getQueryDraftsSelect.ts b/packages/payload/src/versions/drafts/getQueryDraftsSelect.ts new file mode 100644 index 0000000000..af2a80979e --- /dev/null +++ b/packages/payload/src/versions/drafts/getQueryDraftsSelect.ts @@ -0,0 +1,26 @@ +import type { SelectType } from '../../types/index.js' + +import { getSelectMode } from '../../utilities/getSelectMode.js' + +export const getQueryDraftsSelect = ({ + select, +}: { + select?: SelectType +}): SelectType | undefined => { + if (!select) { + return + } + + const mode = getSelectMode(select) + + if (mode === 'include') { + return { + parent: true, + version: select, + } as SelectType + } + + return { + version: select, + } as SelectType +} diff --git a/packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts b/packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts index 4c61999955..c5c9a611ad 100644 --- a/packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts +++ b/packages/payload/src/versions/drafts/replaceWithDraftIfAvailable.ts @@ -2,7 +2,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/co import type { AccessResult } from '../../config/types.js' import type { FindGlobalVersionsArgs, FindVersionsArgs } from '../../database/types.js' import type { SanitizedGlobalConfig } from '../../globals/config/types.js' -import type { PayloadRequest, Where } from '../../types/index.js' +import type { PayloadRequest, SelectType, Where } from '../../types/index.js' import { hasWhereAccessResult } from '../../auth/index.js' import { combineQueries } from '../../database/combineQueries.js' @@ -10,6 +10,7 @@ import { docHasTimestamps } from '../../types/index.js' import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js' import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js' import { appendVersionToQueryKey } from './appendVersionToQueryKey.js' +import { getQueryDraftsSelect } from './getQueryDraftsSelect.js' type Arguments = { accessResult: AccessResult @@ -18,6 +19,7 @@ type Arguments = { entityType: 'collection' | 'global' overrideAccess: boolean req: PayloadRequest + select?: SelectType } const replaceWithDraftIfAvailable = async ({ @@ -26,6 +28,7 @@ const replaceWithDraftIfAvailable = async ({ entity, entityType, req, + select, }: Arguments): Promise => { const { locale } = req @@ -77,6 +80,7 @@ const replaceWithDraftIfAvailable = async ({ locale, pagination: false, req, + select: getQueryDraftsSelect({ select }), sort: '-updatedAt', where: combineQueries(queryToBuild, versionAccessResult), } @@ -102,15 +106,18 @@ const replaceWithDraftIfAvailable = async ({ draft.version.globalType = doc.globalType } + // handle when .version wasn't selected due to projection + if (!draft.version) { + draft.version = {} + } + // Disregard all other draft content at this point, // Only interested in the version itself. // Operations will handle firing hooks, etc. - return { - id: doc.id, - ...draft.version, - createdAt: draft.createdAt, - updatedAt: draft.updatedAt, - } + + draft.version.id = doc.id + + return draft.version } export default replaceWithDraftIfAvailable diff --git a/packages/payload/src/versions/saveVersion.ts b/packages/payload/src/versions/saveVersion.ts index a8b22d4047..14a38ba0d5 100644 --- a/packages/payload/src/versions/saveVersion.ts +++ b/packages/payload/src/versions/saveVersion.ts @@ -1,10 +1,11 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js' import type { SanitizedGlobalConfig } from '../globals/config/types.js' -import type { Payload } from '../index.js' -import type { PayloadRequest } from '../types/index.js' +import type { FindVersionsArgs, Payload } from '../index.js' +import type { PayloadRequest, SelectType } from '../types/index.js' import { deepCopyObjectSimple } from '../index.js' import sanitizeInternalFields from '../utilities/sanitizeInternalFields.js' +import { getQueryDraftsSelect } from './drafts/getQueryDraftsSelect.js' import { enforceMaxVersions } from './enforceMaxVersions.js' type Args = { @@ -17,6 +18,7 @@ type Args = { payload: Payload publishSpecificLocale?: string req?: PayloadRequest + select?: SelectType snapshot?: any } @@ -30,6 +32,7 @@ export const saveVersion = async ({ payload, publishSpecificLocale, req, + select, snapshot, }: Args): Promise => { let result @@ -52,6 +55,7 @@ export const saveVersion = async ({ req, sort: '-updatedAt', } + if (collection) { ;({ docs } = await payload.db.findVersions({ ...findVersionArgs, @@ -121,6 +125,7 @@ export const saveVersion = async ({ parent: collection ? id : undefined, publishedLocale: publishSpecificLocale || undefined, req, + select: getQueryDraftsSelect({ select }), updatedAt: now, versionData, } diff --git a/test/helpers/NextRESTClient.ts b/test/helpers/NextRESTClient.ts index a7922cfc93..efc1293ae9 100644 --- a/test/helpers/NextRESTClient.ts +++ b/test/helpers/NextRESTClient.ts @@ -1,4 +1,4 @@ -import type { JoinQuery, SanitizedConfig, Where } from 'payload' +import type { JoinQuery, SanitizedConfig, SelectType, Where } from 'payload' import type { ParsedQs } from 'qs-esm' import { @@ -22,6 +22,7 @@ type RequestOptions = { limit?: number locale?: string page?: number + select?: SelectType sort?: string where?: Where } @@ -44,15 +45,27 @@ function generateQueryString(query: RequestOptions['query'], params: ParsedQs): } export class NextRESTClient { - private _DELETE: (request: Request, args: { params: { slug: string[] } }) => Promise + private _DELETE: ( + request: Request, + args: { params: Promise<{ slug: string[] }> }, + ) => Promise - private _GET: (request: Request, args: { params: { slug: string[] } }) => Promise + private _GET: ( + request: Request, + args: { params: Promise<{ slug: string[] }> }, + ) => Promise private _GRAPHQL_POST: (request: Request) => Promise - private _PATCH: (request: Request, args: { params: { slug: string[] } }) => Promise + private _PATCH: ( + request: Request, + args: { params: Promise<{ slug: string[] }> }, + ) => Promise - private _POST: (request: Request, args: { params: { slug: string[] } }) => Promise + private _POST: ( + request: Request, + args: { params: Promise<{ slug: string[] }> }, + ) => Promise private readonly config: SanitizedConfig @@ -62,7 +75,9 @@ export class NextRESTClient { constructor(config: SanitizedConfig) { this.config = config - if (config?.serverURL) {this.serverURL = config.serverURL} + if (config?.serverURL) { + this.serverURL = config.serverURL + } this._GET = createGET(config) this._POST = createPOST(config) this._DELETE = createDELETE(config) @@ -118,7 +133,7 @@ export class NextRESTClient { headers: this.buildHeaders(options), method: 'DELETE', }) - return this._DELETE(request, { params: { slug } }) + return this._DELETE(request, { params: Promise.resolve({ slug }) }) } async GET( @@ -134,7 +149,7 @@ export class NextRESTClient { headers: this.buildHeaders(options), method: 'GET', }) - return this._GET(request, { params: { slug } }) + return this._GET(request, { params: Promise.resolve({ slug }) }) } async GRAPHQL_POST(options: RequestInit & RequestOptions): Promise { @@ -190,7 +205,7 @@ export class NextRESTClient { headers: this.buildHeaders(options), method: 'PATCH', }) - return this._PATCH(request, { params: { slug } }) + return this._PATCH(request, { params: Promise.resolve({ slug }) }) } async POST( @@ -204,6 +219,6 @@ export class NextRESTClient { headers: this.buildHeaders(options), method: 'POST', }) - return this._POST(request, { params: { slug } }) + return this._POST(request, { params: Promise.resolve({ slug }) }) } } diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 119b0ebd64..10a6852a93 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -112,6 +112,45 @@ describe('Joins Field', () => { expect(categoryWithPosts.group.relatedPosts.docs[0].title).toStrictEqual('test 9') }) + it('should not populate joins if not selected', async () => { + const categoryWithPosts = await payload.findByID({ + id: category.id, + joins: { + 'group.relatedPosts': { + sort: '-title', + }, + }, + select: {}, + collection: 'categories', + }) + + expect(Object.keys(categoryWithPosts)).toStrictEqual(['id']) + }) + + it('should populate joins if selected', async () => { + const categoryWithPosts = await payload.findByID({ + id: category.id, + joins: { + 'group.relatedPosts': { + sort: '-title', + }, + }, + select: { + group: { + relatedPosts: true, + }, + }, + collection: 'categories', + }) + + expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', 'group']) + + expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10) + expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id') + expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('title') + expect(categoryWithPosts.group.relatedPosts.docs[0].title).toStrictEqual('test 9') + }) + it('should populate relationships in joins', async () => { const { docs } = await payload.find({ limit: 1, diff --git a/test/joins/payload-types.ts b/test/joins/payload-types.ts index 075594ca7d..f388f2472d 100644 --- a/test/joins/payload-types.ts +++ b/test/joins/payload-types.ts @@ -23,10 +23,24 @@ export interface Config { 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; }; + collectionsSelect?: { + posts: PostsSelect | PostsSelect; + categories: CategoriesSelect | CategoriesSelect; + uploads: UploadsSelect | UploadsSelect; + versions: VersionsSelect | VersionsSelect; + 'categories-versions': CategoriesVersionsSelect | CategoriesVersionsSelect; + 'localized-posts': LocalizedPostsSelect | LocalizedPostsSelect; + 'localized-categories': LocalizedCategoriesSelect | LocalizedCategoriesSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; db: { defaultIDType: string; }; globals: {}; + globalsSelect?: {}; locale: 'en' | 'es'; user: User & { collection: 'users'; @@ -271,6 +285,149 @@ export interface PayloadMigration { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts_select". + */ +export interface PostsSelect { + title?: T; + upload?: T; + category?: T; + categories?: T; + categoriesLocalized?: T; + group?: + | T + | { + category?: T; + camelCaseCategory?: T; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "categories_select". + */ +export interface CategoriesSelect { + name?: T; + relatedPosts?: T; + hasManyPosts?: T; + hasManyPostsLocalized?: T; + group?: + | T + | { + relatedPosts?: T; + camelCasePosts?: T; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "uploads_select". + */ +export interface UploadsSelect { + relatedPosts?: T; + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "versions_select". + */ +export interface VersionsSelect { + category?: T; + categoryVersion?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "categories-versions_select". + */ +export interface CategoriesVersionsSelect { + relatedVersions?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localized-posts_select". + */ +export interface LocalizedPostsSelect { + title?: T; + category?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localized-categories_select". + */ +export interface LocalizedCategoriesSelect { + name?: T; + relatedPosts?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". diff --git a/test/select/.gitignore b/test/select/.gitignore new file mode 100644 index 0000000000..cce01755f4 --- /dev/null +++ b/test/select/.gitignore @@ -0,0 +1,2 @@ +/media +/media-gif diff --git a/test/select/collections/DeepPosts/index.ts b/test/select/collections/DeepPosts/index.ts new file mode 100644 index 0000000000..5ae289605f --- /dev/null +++ b/test/select/collections/DeepPosts/index.ts @@ -0,0 +1,76 @@ +import type { CollectionConfig } from 'payload' + +export const DeepPostsCollection: CollectionConfig = { + slug: 'deep-posts', + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'array', + type: 'array', + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'block', + fields: [ + { + type: 'text', + name: 'text', + }, + { + type: 'number', + name: 'number', + }, + ], + }, + ], + }, + ], + }, + { + name: 'arrayTop', + type: 'array', + fields: [ + { + type: 'text', + name: 'text', + }, + { + type: 'array', + name: 'arrayNested', + fields: [ + { + type: 'text', + name: 'text', + }, + { + type: 'number', + name: 'number', + }, + ], + }, + ], + }, + ], +} diff --git a/test/select/collections/LocalizedPosts/index.ts b/test/select/collections/LocalizedPosts/index.ts new file mode 100644 index 0000000000..5004456723 --- /dev/null +++ b/test/select/collections/LocalizedPosts/index.ts @@ -0,0 +1,147 @@ +import type { CollectionConfig } from 'payload' + +export const LocalizedPostsCollection: CollectionConfig = { + slug: 'localized-posts', + admin: { + useAsTitle: 'text', + }, + fields: [ + { + name: 'text', + localized: true, + type: 'text', + }, + { + name: 'number', + localized: true, + type: 'number', + }, + { + name: 'group', + localized: true, + type: 'group', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'groupSecond', + type: 'group', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'array', + type: 'array', + localized: true, + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'arraySecond', + type: 'array', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + localized: true, + blocks: [ + { + slug: 'intro', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'introText', + type: 'text', + }, + ], + }, + { + slug: 'cta', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'ctaText', + type: 'text', + }, + ], + }, + ], + }, + { + name: 'blocksSecond', + type: 'blocks', + blocks: [ + { + slug: 'first', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'firstText', + type: 'text', + localized: true, + }, + ], + }, + { + slug: 'second', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + { + name: 'secondText', + type: 'text', + }, + ], + }, + ], + }, + ], +} diff --git a/test/select/collections/Posts/index.ts b/test/select/collections/Posts/index.ts new file mode 100644 index 0000000000..d0e0c3f3e7 --- /dev/null +++ b/test/select/collections/Posts/index.ts @@ -0,0 +1,78 @@ +import type { CollectionConfig } from 'payload' + +export const PostsCollection: CollectionConfig = { + slug: 'posts', + admin: { + useAsTitle: 'text', + }, + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + { + name: 'group', + type: 'group', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'array', + type: 'array', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'intro', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'introText', + type: 'text', + }, + ], + }, + { + slug: 'cta', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'ctaText', + type: 'text', + }, + ], + }, + ], + }, + ], +} diff --git a/test/select/collections/VersionedPosts/index.ts b/test/select/collections/VersionedPosts/index.ts new file mode 100644 index 0000000000..2d19f41e06 --- /dev/null +++ b/test/select/collections/VersionedPosts/index.ts @@ -0,0 +1,43 @@ +import type { CollectionConfig } from 'payload' + +export const VersionedPostsCollection: CollectionConfig = { + slug: 'versioned-posts', + versions: { + drafts: true, + }, + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + { + name: 'array', + type: 'array', + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + { + name: 'blocks', + type: 'blocks', + blocks: [ + { + slug: 'test', + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + ], + }, + ], +} diff --git a/test/select/config.ts b/test/select/config.ts new file mode 100644 index 0000000000..00e5710183 --- /dev/null +++ b/test/select/config.ts @@ -0,0 +1,73 @@ +import { lexicalEditor } from '@payloadcms/richtext-lexical' +import { fileURLToPath } from 'node:url' +import path from 'path' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { devUser } from '../credentials.js' +import { DeepPostsCollection } from './collections/DeepPosts/index.js' +import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js' +import { PostsCollection } from './collections/Posts/index.js' +import { VersionedPostsCollection } from './collections/VersionedPosts/index.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export default buildConfigWithDefaults({ + // ...extend config here + collections: [ + PostsCollection, + LocalizedPostsCollection, + VersionedPostsCollection, + DeepPostsCollection, + ], + globals: [ + { + slug: 'global-post', + fields: [ + { + name: 'text', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + ], + }, + ], + admin: { + importMap: { + baseDir: path.resolve(dirname), + }, + }, + localization: { + locales: ['en', 'de'], + defaultLocale: 'en', + }, + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [...defaultFeatures], + }), + cors: ['http://localhost:3000', 'http://localhost:3001'], + onInit: async (payload) => { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + + // // Create image + // const imageFilePath = path.resolve(dirname, '../uploads/image.png') + // const imageFile = await getFileByPath(imageFilePath) + + // await payload.create({ + // collection: 'media', + // data: {}, + // file: imageFile, + // }) + }, + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, +}) diff --git a/test/select/eslint.config.js b/test/select/eslint.config.js new file mode 100644 index 0000000000..f295df083f --- /dev/null +++ b/test/select/eslint.config.js @@ -0,0 +1,19 @@ +import { rootParserOptions } from '../../eslint.config.js' +import { testEslintConfig } from '../eslint.config.js' + +/** @typedef {import('eslint').Linter.Config} Config */ + +/** @type {Config[]} */ +export const index = [ + ...testEslintConfig, + { + languageOptions: { + parserOptions: { + ...rootParserOptions, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +] + +export default index diff --git a/test/select/int.spec.ts b/test/select/int.spec.ts new file mode 100644 index 0000000000..86b1fe02f7 --- /dev/null +++ b/test/select/int.spec.ts @@ -0,0 +1,1677 @@ +import path from 'path' +import { deepCopyObject, type Payload } from 'payload' +import { fileURLToPath } from 'url' + +import type { NextRESTClient } from '../helpers/NextRESTClient.js' +import type { + Config, + DeepPost, + GlobalPost, + LocalizedPost, + Post, + VersionedPost, +} from './payload-types.js' + +import { initPayloadInt } from '../helpers/initPayloadInt.js' + +let payload: Payload +let restClient: NextRESTClient + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +describe('Select', () => { + // --__--__--__--__--__--__--__--__--__ + // Boilerplate test setup/teardown + // --__--__--__--__--__--__--__--__--__ + beforeAll(async () => { + const initialized = await initPayloadInt(dirname) + ;({ payload, restClient } = initialized) + }) + + afterAll(async () => { + if (typeof payload.db.destroy === 'function') { + await payload.db.destroy() + } + }) + + describe('Local API - Base', () => { + let post: Post + let postId: number | string + + beforeEach(async () => { + post = await createPost() + postId = post.id + }) + + // Clean up to safely mutate in each test + afterEach(async () => { + await payload.delete({ id: postId, collection: 'posts' }) + }) + + describe('Include mode', () => { + it('should select only id as default', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: {}, + }) + + expect(res).toStrictEqual({ + id: postId, + }) + }) + + it('should select only number', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + number: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + }) + }) + + it('should select number and text', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + number: true, + text: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + text: post.text, + }) + }) + + it('should select all the fields inside of group', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + group: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: post.group, + }) + }) + + it('should select text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + group: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: { + text: post.group.text, + }, + }) + }) + + it('should select id as default from array', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + array: {}, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array.map((item) => ({ id: item.id })), + }) + }) + + it('should select all the fields inside of array', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + array: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array, + }) + }) + + it('should select text field inside of array', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + array: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array.map((item) => ({ + id: item.id, + text: item.text, + })), + }) + }) + + it('should select base fields (id, blockType) inside of blocks', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: {}, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => ({ blockType: block.blockType, id: block.id })), + }) + }) + + it('should select all the fields inside of blocks', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks, + }) + }) + + it('should select all the fields inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: { + cta: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' + ? block + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + + it('should select a specific field inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: { + cta: { ctaText: true }, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' + ? { id: block.id, blockType: block.blockType, ctaText: block.ctaText } + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + }) + + describe('Exclude mode', () => { + it('should exclude only text field', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + text: false, + }, + }) + + const expected = { ...post } + + delete expected['text'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude number', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + number: false, + }, + }) + + const expected = { ...post } + + delete expected['number'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude number and text', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + number: false, + text: false, + }, + }) + + const expected = { ...post } + + delete expected['text'] + delete expected['number'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude group', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + group: false, + }, + }) + + const expected = { ...post } + + delete expected['group'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + group: { + text: false, + }, + }, + }) + + const expected = deepCopyObject(post) + + delete expected.group.text + + expect(res).toStrictEqual(expected) + }) + + it('should exclude array', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + array: false, + }, + }) + + const expected = { ...post } + + delete expected['array'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude text field inside of array', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + array: { + text: false, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + array: post.array.map((item) => ({ + id: item.id, + number: item.number, + })), + }) + }) + + it('should exclude blocks', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: false, + }, + }) + + const expected = { ...post } + + delete expected['blocks'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude all the fields inside of specific block while keeping base fields', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: { + cta: false, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' ? { id: block.id, blockType: block.blockType } : block, + ), + }) + }) + + it('should exclude a specific field inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + blocks: { + cta: { ctaText: false }, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocks: post.blocks.map((block) => { + delete block['ctaText'] + + return block + }), + }) + }) + }) + }) + + describe('Local API - Localization', () => { + let post: LocalizedPost + let postId: number | string + + beforeEach(async () => { + post = await createLocalizedPost() + postId = post.id + }) + + // Clean up to safely mutate in each test + afterEach(async () => { + await payload.delete({ id: postId, collection: 'localized-posts' }) + }) + + describe('Include mode', () => { + it('should select only id as default', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: {}, + }) + + expect(res).toStrictEqual({ + id: postId, + }) + }) + + it('should select only number', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + number: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + }) + }) + + it('should select number and text', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + number: true, + text: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + text: post.text, + }) + }) + + it('should select all the fields inside of group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + group: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: post.group, + }) + }) + + it('should select text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + group: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: { + text: post.group.text, + }, + }) + }) + + it('should select localized text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + groupSecond: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + groupSecond: { + text: post.groupSecond.text, + }, + }) + }) + + it('should select id as default from array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + array: {}, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array.map((item) => ({ id: item.id })), + }) + }) + + it('should select all the fields inside of array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + array: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array, + }) + }) + + it('should select text field inside of localized array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + array: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + array: post.array.map((item) => ({ + id: item.id, + text: item.text, + })), + }) + }) + + it('should select localized text field inside of array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + arraySecond: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + arraySecond: post.arraySecond.map((item) => ({ + id: item.id, + text: item.text, + })), + }) + }) + + it('should select base fields (id, blockType) inside of blocks', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: {}, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => ({ blockType: block.blockType, id: block.id })), + }) + }) + + it('should select all the fields inside of blocks', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks, + }) + }) + + it('should select all the fields inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: { + cta: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' + ? block + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + + it('should select a specific field inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: { + cta: { ctaText: true }, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' + ? { id: block.id, blockType: block.blockType, ctaText: block.ctaText } + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + + it('should select a specific localized field inside of specific block 1', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocksSecond: { + second: { text: true }, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocksSecond: post.blocksSecond.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'second' + ? { id: block.id, blockType: block.blockType, text: block.text } + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + + it('should select a specific localized field inside of specific block 2', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocksSecond: { + first: { firstText: true }, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + blocksSecond: post.blocksSecond.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'first' + ? { id: block.id, blockType: block.blockType, firstText: block.firstText } + : { + id: block.id, + blockType: block.blockType, + }, + ), + }) + }) + }) + + describe('Exclude mode', () => { + it('should exclude only text field', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + text: false, + }, + }) + + const expected = { ...post } + + delete expected['text'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude number', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + number: false, + }, + }) + + const expected = { ...post } + + delete expected['number'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude number and text', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + number: false, + text: false, + }, + }) + + const expected = { ...post } + + delete expected['text'] + delete expected['number'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + group: false, + }, + }) + + const expected = { ...post } + + delete expected['group'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + group: { + text: false, + }, + }, + }) + + const expected = deepCopyObject(post) + + delete expected.group.text + + expect(res).toStrictEqual(expected) + }) + + it('should exclude localized text field inside of group', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + groupSecond: { + text: false, + }, + }, + }) + + const expected = deepCopyObject(post) + + delete expected.groupSecond.text + + expect(res).toStrictEqual(expected) + }) + + it('should exclude array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + array: false, + }, + }) + + const expected = { ...post } + + delete expected['array'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude text field inside of array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + array: { + text: false, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + array: post.array.map((item) => ({ + id: item.id, + number: item.number, + })), + }) + }) + + it('should exclude localized text field inside of array', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + arraySecond: { + text: false, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + arraySecond: post.arraySecond.map((item) => ({ + id: item.id, + number: item.number, + })), + }) + }) + + it('should exclude blocks', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: false, + }, + }) + + const expected = { ...post } + + delete expected['blocks'] + + expect(res).toStrictEqual(expected) + }) + + it('should exclude all the fields inside of specific block while keeping base fields', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: { + cta: false, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocks: post.blocks.map((block) => + // eslint-disable-next-line jest/no-conditional-in-test + block.blockType === 'cta' ? { id: block.id, blockType: block.blockType } : block, + ), + }) + }) + + it('should exclude a specific field inside of specific block', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocks: { + cta: { ctaText: false }, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocks: post.blocks.map((block) => { + delete block['ctaText'] + + return block + }), + }) + }) + + it('should exclude a specific localized field inside of specific block 1', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocksSecond: { + second: { text: false }, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocksSecond: post.blocksSecond.map((block) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (block.blockType === 'second') { + delete block['text'] + } + + return block + }), + }) + }) + + it('should exclude a specific localized field inside of specific block 2', async () => { + const res = await payload.findByID({ + collection: 'localized-posts', + id: postId, + select: { + blocksSecond: { + first: { firstText: false }, + }, + }, + }) + + expect(res).toStrictEqual({ + ...post, + blocksSecond: post.blocksSecond.map((block) => { + delete block['firstText'] + + return block + }), + }) + }) + }) + }) + + describe('Local API - Deep Fields', () => { + let post: DeepPost + let postId: number | string + + beforeEach(async () => { + post = await createDeepPost() + postId = post.id + }) + + it('should select deply group.array.group.text', async () => { + const res = await payload.findByID({ + id: postId, + collection: 'deep-posts', + select: { group: { array: { group: { text: true } } } }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: { + array: post.group.array.map((item) => ({ + id: item.id, + group: { + text: item.group.text, + }, + })), + }, + }) + }) + + it('should select deply group.array.group.*', async () => { + const res = await payload.findByID({ + id: postId, + collection: 'deep-posts', + select: { group: { array: { group: true } } }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: { + array: post.group.array.map((item) => ({ + id: item.id, + group: item.group, + })), + }, + }) + }) + + it('should select deply group.blocks.block.text', async () => { + const res = await payload.findByID({ + id: postId, + collection: 'deep-posts', + select: { group: { blocks: { block: { text: true } } } }, + }) + + expect(res).toStrictEqual({ + id: postId, + group: { + blocks: post.group.blocks.map((item) => ({ + id: item.id, + blockType: item.blockType, + text: item.text, + })), + }, + }) + }) + + it('should select deply array.array.text', async () => { + const res = await payload.findByID({ + id: postId, + collection: 'deep-posts', + select: { arrayTop: { arrayNested: { text: true } } }, + }) + + expect(res).toStrictEqual({ + id: postId, + arrayTop: post.arrayTop.map((item) => ({ + id: item.id, + arrayNested: item.arrayNested.map((item) => ({ + id: item.id, + text: item.text, + })), + })), + }) + }) + }) + + describe('Local API - Versioned Drafts Collection', () => { + let post: VersionedPost + let postId: number | string + + beforeEach(async () => { + post = await createVersionedPost() + postId = post.id + }) + + // Clean up to safely mutate in each test + afterEach(async () => { + await payload.delete({ id: postId, collection: 'versioned-posts' }) + }) + + it('should select only id as default', async () => { + const res = await payload.findByID({ + collection: 'versioned-posts', + id: postId, + select: {}, + draft: true, + }) + + expect(res).toStrictEqual({ + id: postId, + }) + }) + + it('should select only number', async () => { + const res = await payload.findByID({ + collection: 'versioned-posts', + id: postId, + select: { + number: true, + }, + draft: true, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + }) + }) + + it('should exclude only number', async () => { + const res = await payload.findByID({ + collection: 'versioned-posts', + id: postId, + select: { + number: false, + }, + draft: true, + }) + + const expected = { ...post } + + delete expected['number'] + expect(res).toStrictEqual(expected) + }) + + it('should select number and text', async () => { + const res = await payload.findByID({ + collection: 'versioned-posts', + id: postId, + select: { + number: true, + text: true, + }, + draft: true, + }) + + expect(res).toStrictEqual({ + id: postId, + number: post.number, + text: post.text, + }) + }) + + it('payload.find should select number and text', async () => { + const res = await payload.find({ + collection: 'versioned-posts', + where: { + id: { + equals: postId, + }, + }, + select: { + number: true, + text: true, + }, + draft: true, + }) + + expect(res.docs[0]).toStrictEqual({ + id: postId, + number: post.number, + text: post.text, + }) + }) + + it('should select base id field inside of array', async () => { + const res = await payload.find({ + collection: 'versioned-posts', + where: { + id: { + equals: postId, + }, + }, + select: { + array: {}, + }, + draft: true, + }) + + expect(res.docs[0]).toStrictEqual({ + id: postId, + array: post.array.map((each) => ({ id: each.id })), + }) + }) + + it('should select base id field inside of blocks', async () => { + const res = await payload.find({ + collection: 'versioned-posts', + where: { + id: { + equals: postId, + }, + }, + select: { + blocks: {}, + }, + draft: true, + }) + + expect(res.docs[0]).toStrictEqual({ + id: postId, + blocks: post.blocks.map((each) => ({ blockType: each.blockType, id: each.id })), + }) + }) + + it('should select with payload.findVersions', async () => { + const res = await payload.findVersions({ + collection: 'versioned-posts', + limit: 1, + sort: '-updatedAt', + where: { parent: { equals: postId } }, + select: { + version: { + text: true, + }, + }, + }) + + // findVersions doesnt transform result with afterRead hook and so doesn't strip undefined values from the object + // undefined values are happened with drizzle adapters because of transform/read + + const doc = res.docs[0] + + expect(doc.createdAt).toBeUndefined() + expect(doc.updatedAt).toBeUndefined() + expect(doc.version.number).toBeUndefined() + expect(doc.version.createdAt).toBeUndefined() + expect(doc.version.text).toBe(post.text) + }) + }) + + describe('Local API - Globals', () => { + let globalPost: GlobalPost + beforeAll(async () => { + globalPost = await payload.updateGlobal({ + slug: 'global-post', + data: { + number: 2, + text: '3', + }, + }) + }) + + it('should select with find', async () => { + const res = await payload.findGlobal({ + slug: 'global-post', + select: { + text: true, + }, + }) + + expect(res).toStrictEqual({ + id: globalPost.id, + text: globalPost.text, + }) + }) + + it('should select with update', async () => { + const res = await payload.updateGlobal({ + slug: 'global-post', + data: {}, + select: { + text: true, + }, + }) + + expect(res).toStrictEqual({ + id: globalPost.id, + text: globalPost.text, + }) + }) + }) + + describe('Local API - operations', () => { + it('should apply select with create', async () => { + const res = await payload.create({ + collection: 'posts', + data: { + text: 'asd', + number: 123, + }, + select: { + text: true, + }, + }) + + expect(Object.keys(res)).toStrictEqual(['id', 'text']) + }) + + it('should apply select with updateByID', async () => { + const post = await createPost() + + const res = await payload.update({ + collection: 'posts', + id: post.id, + data: {}, + select: { text: true }, + }) + + expect(Object.keys(res)).toStrictEqual(['id', 'text']) + }) + + it('should apply select with updateBulk', async () => { + const post = await createPost() + + const res = await payload.update({ + collection: 'posts', + where: { + id: { + equals: post.id, + }, + }, + data: {}, + select: { text: true }, + }) + + expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + }) + + it('should apply select with deleteByID', async () => { + const post = await createPost() + + const res = await payload.delete({ + collection: 'posts', + id: post.id, + select: { text: true }, + }) + + expect(Object.keys(res)).toStrictEqual(['id', 'text']) + }) + + it('should apply select with deleteBulk', async () => { + const post = await createPost() + + const res = await payload.delete({ + collection: 'posts', + where: { + id: { + equals: post.id, + }, + }, + select: { text: true }, + }) + + expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + }) + + it('should apply select with duplicate', async () => { + const post = await createPost() + + const res = await payload.duplicate({ + collection: 'posts', + id: post.id, + select: { text: true }, + }) + + expect(Object.keys(res)).toStrictEqual(['id', 'text']) + }) + }) + + describe('REST API - Base', () => { + let post: Post + let postId: number | string + + beforeEach(async () => { + post = await createPost() + postId = post.id + }) + + // Clean up to safely mutate in each test + afterEach(async () => { + await payload.delete({ id: postId, collection: 'posts' }) + }) + + describe('Include mode', () => { + it('should select only text', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + text: true, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + expect(res).toMatchObject({ + text: post.text, + id: postId, + }) + }) + + it('should select number and text', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + number: true, + text: true, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + expect(res).toMatchObject({ + id: postId, + number: post.number, + text: post.text, + }) + }) + + it('should select all the fields inside of group', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + group: true, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + expect(res).toMatchObject({ + id: postId, + group: post.group, + }) + }) + + it('should select text field inside of group', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + group: { text: true }, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + expect(res).toMatchObject({ + id: postId, + group: { + text: post.group.text, + }, + }) + }) + }) + + describe('Exclude mode', () => { + it('should exclude only text field', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + text: false, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + const expected = { ...post } + + delete expected['text'] + + expect(res).toMatchObject(expected) + }) + + it('should exclude number', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + number: false, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + const expected = { ...post } + + delete expected['number'] + + expect(res).toMatchObject(expected) + }) + + it('should exclude number and text', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + number: false, + text: false, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + const expected = { ...post } + + delete expected['text'] + delete expected['number'] + + expect(res).toMatchObject(expected) + }) + + it('should exclude text field inside of group', async () => { + const res = await restClient + .GET(`/posts/${postId}`, { + query: { + select: { + group: { + text: false, + }, + } satisfies Config['collectionsSelect']['posts'], + }, + }) + .then((res) => res.json()) + + const expected = deepCopyObject(post) + + delete expected.group.text + + expect(res).toMatchObject(expected) + }) + }) + }) +}) + +function createPost() { + return payload.create({ + collection: 'posts', + depth: 0, + data: { + number: 1, + text: 'text', + group: { + number: 1, + text: 'text', + }, + blocks: [ + { + blockType: 'cta', + ctaText: 'cta-text', + text: 'text', + }, + { + blockType: 'intro', + introText: 'intro-text', + text: 'text', + }, + ], + array: [ + { + text: 'text', + number: 1, + }, + ], + }, + }) +} + +function createLocalizedPost() { + return payload.create({ + collection: 'localized-posts', + depth: 0, + data: { + number: 1, + text: 'text', + group: { + number: 1, + text: 'text', + }, + groupSecond: { + number: 1, + text: 'text', + }, + blocks: [ + { + blockType: 'cta', + ctaText: 'cta-text', + text: 'text', + }, + { + blockType: 'intro', + introText: 'intro-text', + text: 'text', + }, + ], + blocksSecond: [ + { + blockType: 'second', + secondText: 'cta-text', + text: 'text', + }, + { + blockType: 'first', + firstText: 'intro-text', + text: 'text', + }, + ], + array: [ + { + text: 'text', + number: 1, + }, + ], + arraySecond: [ + { + text: 'text', + number: 1, + }, + ], + }, + }) +} + +function createDeepPost() { + return payload.create({ + collection: 'deep-posts', + data: { + arrayTop: [{ text: 'text1', arrayNested: [{ text: 'text2', number: 34 }] }], + group: { + array: [{ group: { number: 1, text: 'text-3' } }], + blocks: [{ blockType: 'block', number: 3, text: 'text-4' }], + }, + }, + }) +} + +function createVersionedPost() { + return payload.create({ + collection: 'versioned-posts', + data: { + number: 2, + text: 'text', + array: [{ text: 'hello' }], + blocks: [{ blockType: 'test', text: 'hela' }], + }, + }) +} diff --git a/test/select/payload-types.ts b/test/select/payload-types.ts new file mode 100644 index 0000000000..664e8d4a2e --- /dev/null +++ b/test/select/payload-types.ts @@ -0,0 +1,604 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + posts: Post; + 'localized-posts': LocalizedPost; + 'versioned-posts': VersionedPost; + 'deep-posts': DeepPost; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsSelect?: { + posts: PostsSelect | PostsSelect; + 'localized-posts': LocalizedPostsSelect | LocalizedPostsSelect; + 'versioned-posts': VersionedPostsSelect | VersionedPostsSelect; + 'deep-posts': DeepPostsSelect | DeepPostsSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: { + 'global-post': GlobalPost; + }; + globalsSelect?: { + 'global-post': GlobalPostSelect | GlobalPostSelect; + }; + locale: 'en' | 'de'; + user: User & { + collection: 'users'; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + text?: string | null; + number?: number | null; + group?: { + text?: string | null; + number?: number | null; + }; + array?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + blocks?: + | ( + | { + text?: string | null; + introText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'intro'; + } + | { + text?: string | null; + ctaText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'cta'; + } + )[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localized-posts". + */ +export interface LocalizedPost { + id: string; + text?: string | null; + number?: number | null; + group?: { + text?: string | null; + number?: number | null; + }; + groupSecond?: { + text?: string | null; + number?: number | null; + }; + array?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + arraySecond?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + blocks?: + | ( + | { + text?: string | null; + introText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'intro'; + } + | { + text?: string | null; + ctaText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'cta'; + } + )[] + | null; + blocksSecond?: + | ( + | { + text?: string | null; + firstText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'first'; + } + | { + text?: string | null; + secondText?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'second'; + } + )[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "versioned-posts". + */ +export interface VersionedPost { + id: string; + text?: string | null; + number?: number | null; + array?: + | { + text?: string | null; + id?: string | null; + }[] + | null; + blocks?: + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'test'; + }[] + | null; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "deep-posts". + */ +export interface DeepPost { + id: string; + group?: { + array?: + | { + group?: { + text?: string | null; + number?: number | null; + }; + id?: string | null; + }[] + | null; + blocks?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + blockName?: string | null; + blockType: 'block'; + }[] + | null; + }; + arrayTop?: + | { + text?: string | null; + arrayNested?: + | { + text?: string | null; + number?: number | null; + id?: string | null; + }[] + | null; + id?: string | null; + }[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: + | ({ + relationTo: 'posts'; + value: string | Post; + } | null) + | ({ + relationTo: 'localized-posts'; + value: string | LocalizedPost; + } | null) + | ({ + relationTo: 'versioned-posts'; + value: string | VersionedPost; + } | null) + | ({ + relationTo: 'deep-posts'; + value: string | DeepPost; + } | null) + | ({ + relationTo: 'users'; + value: string | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts_select". + */ +export interface PostsSelect { + text?: T; + number?: T; + group?: + | T + | { + text?: T; + number?: T; + }; + array?: + | T + | { + text?: T; + number?: T; + id?: T; + }; + blocks?: + | T + | { + intro?: + | T + | { + text?: T; + introText?: T; + id?: T; + blockName?: T; + }; + cta?: + | T + | { + text?: T; + ctaText?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localized-posts_select". + */ +export interface LocalizedPostsSelect { + text?: T; + number?: T; + group?: + | T + | { + text?: T; + number?: T; + }; + groupSecond?: + | T + | { + text?: T; + number?: T; + }; + array?: + | T + | { + text?: T; + number?: T; + id?: T; + }; + arraySecond?: + | T + | { + text?: T; + number?: T; + id?: T; + }; + blocks?: + | T + | { + intro?: + | T + | { + text?: T; + introText?: T; + id?: T; + blockName?: T; + }; + cta?: + | T + | { + text?: T; + ctaText?: T; + id?: T; + blockName?: T; + }; + }; + blocksSecond?: + | T + | { + first?: + | T + | { + text?: T; + firstText?: T; + id?: T; + blockName?: T; + }; + second?: + | T + | { + text?: T; + secondText?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "versioned-posts_select". + */ +export interface VersionedPostsSelect { + text?: T; + number?: T; + array?: + | T + | { + text?: T; + id?: T; + }; + blocks?: + | T + | { + test?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: T; + _status?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "deep-posts_select". + */ +export interface DeepPostsSelect { + group?: + | T + | { + array?: + | T + | { + group?: + | T + | { + text?: T; + number?: T; + }; + id?: T; + }; + blocks?: + | T + | { + block?: + | T + | { + text?: T; + number?: T; + id?: T; + blockName?: T; + }; + }; + }; + arrayTop?: + | T + | { + text?: T; + arrayNested?: + | T + | { + text?: T; + number?: T; + id?: T; + }; + id?: T; + }; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "global-post". + */ +export interface GlobalPost { + id: string; + text?: string | null; + number?: number | null; + updatedAt?: string | null; + createdAt?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "global-post_select". + */ +export interface GlobalPostSelect { + text?: T; + number?: T; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} \ No newline at end of file diff --git a/test/select/tsconfig.eslint.json b/test/select/tsconfig.eslint.json new file mode 100644 index 0000000000..b34cc7afbb --- /dev/null +++ b/test/select/tsconfig.eslint.json @@ -0,0 +1,13 @@ +{ + // extend your base config to share compilerOptions, etc + //"extends": "./tsconfig.json", + "compilerOptions": { + // ensure that nobody can accidentally use this config for a build + "noEmit": true + }, + "include": [ + // whatever paths you intend to lint + "./**/*.ts", + "./**/*.tsx" + ] +} diff --git a/test/select/tsconfig.json b/test/select/tsconfig.json new file mode 100644 index 0000000000..3c43903cfd --- /dev/null +++ b/test/select/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +}