From 25151ee19129a57093689778f30fcf3688e2e003 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Fri, 16 Jun 2023 16:58:36 -0400 Subject: [PATCH] chore: collection based preferences (#2820) **BREAKING CHANGES** Preferences have been overhauled to be abstracted as a Payload collection and no longer explicitly defined by Payload. They previously used the slug `_preferences` as a collection name and url route and are now If any of the following are true you will need to take action: - You have existing preferences you wish to keep for your admin users you must migrate data in the _preferences collection to the new shape. To migrate the preferences in the database you must update the shape of each _preferences document from: ```js { user: ObjectId("abc"), userCollection: "users", /** other fields remain the same **/ } ``` to: ```js { user: { value: 'abc', relationTo: 'users", } /** other fields remain the same **/ } ``` - You have code external of Payload or custom code within Payload using the API endpoint `api/_preferences`, you should update any applications to use `api/payload-preferences` instead. - You were using the preferences GraphQL implementation. This was removed and is instead provided the same way as Payload handles any other. In this way the queries, mutation and schemas have changed. These are now generated as any other collection within your payload project. - You were using the Payload's exported Preference type for your typescript code. Now you can instead import the generated type from your project. --- docs/admin/preferences.mdx | 20 ++--- docs/rest-api/overview.mdx | 6 +- .../utilities/Preferences/index.tsx | 4 +- src/auth/strategies/apiKey.ts | 4 +- src/collections/initHTTP.ts | 6 +- src/collections/operations/create.ts | 2 +- src/collections/operations/delete.ts | 23 ++--- src/collections/operations/deleteByID.ts | 11 +-- src/config/sanitize.ts | 3 + src/graphql/registerSchema.ts | 2 - src/initHTTP.ts | 2 - src/payload.ts | 5 -- src/preferences/deleteUserPreferences.ts | 20 +++++ src/preferences/graphql/init.ts | 64 -------------- src/preferences/init.ts | 18 ---- src/preferences/model.ts | 15 ---- src/preferences/operations/delete.ts | 31 ++++--- src/preferences/operations/findOne.ts | 44 +++++----- src/preferences/operations/update.ts | 37 +++++--- src/preferences/preferencesCollection.ts | 84 +++++++++++++++++++ src/preferences/requestHandlers/delete.ts | 2 +- src/preferences/requestHandlers/findOne.ts | 4 +- src/preferences/requestHandlers/update.ts | 6 +- src/preferences/types.ts | 14 ---- 24 files changed, 217 insertions(+), 210 deletions(-) create mode 100644 src/preferences/deleteUserPreferences.ts delete mode 100644 src/preferences/graphql/init.ts delete mode 100644 src/preferences/init.ts delete mode 100644 src/preferences/model.ts create mode 100644 src/preferences/preferencesCollection.ts diff --git a/docs/admin/preferences.mdx b/docs/admin/preferences.mdx index 38ae631836..4343acde60 100644 --- a/docs/admin/preferences.mdx +++ b/docs/admin/preferences.mdx @@ -30,17 +30,17 @@ This API is used significantly for internal operations of the Admin panel, as me ### Database -Payload automatically creates an internally used `_preferences` collection that stores user preferences. Each document in the `_preferences` collection contains the following shape: +Payload automatically creates an internally used `payload-preferences` collection that stores user preferences. Each document in the `payload-preferences` collection contains the following shape: -| Key | Value | -| -------------------- | -------------| -| `id` | A unique ID for each preference stored. | -| `key` | A unique `key` that corresponds to the preference. | -| `user` | The ID of the `user` that is storing its preference. | -| `userCollection` | The `slug` of the collection that the `user` is logged in as. | -| `value` | The value of the preference. Can be any data shape that you need. | -| `createdAt` | A timestamp of when the preference was created. | -| `updatedAt` | A timestamp set to the last time the preference was updated. +| Key | Value | +|-------------------|-------------------------------------------------------------------| +| `id` | A unique ID for each preference stored. | +| `key` | A unique `key` that corresponds to the preference. | +| `user.value` | The ID of the `user` that is storing its preference. | +| `user.relationTo` | The `slug` of the collection that the `user` is logged in as. | +| `value` | The value of the preference. Can be any data shape that you need. | +| `createdAt` | A timestamp of when the preference was created. | +| `updatedAt` | A timestamp set to the last time the preference was updated. | ### APIs diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index 92e072b7a8..fcdcfda6fa 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -479,7 +479,7 @@ In addition to the dynamically generated endpoints above Payload also has REST e { operation: "Get Preference", method: "GET", - path: "/api/_preferences/{key}", + path: "/api/payload-preferences/{key}", description: "Get a preference by key", example: { slug: "getPreference", @@ -501,7 +501,7 @@ In addition to the dynamically generated endpoints above Payload also has REST e { operation: "Create Preference", method: "POST", - path: "/api/_preferences/{key}", + path: "/api/payload-preferences/{key}", description: "Create or update a preference by key", example: { slug: "createPreference", @@ -525,7 +525,7 @@ In addition to the dynamically generated endpoints above Payload also has REST e { operation: "Delete Preference", method: "DELETE", - path: "/api/_preferences/{key}", + path: "/api/payload-preferences/{key}", description: "Delete a preference by key", example: { slug: "deletePreference", diff --git a/src/admin/components/utilities/Preferences/index.tsx b/src/admin/components/utilities/Preferences/index.tsx index 633d29b20a..90d457e8bb 100644 --- a/src/admin/components/utilities/Preferences/index.tsx +++ b/src/admin/components/utilities/Preferences/index.tsx @@ -40,7 +40,7 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch if (typeof prefs[key] !== 'undefined') return prefs[key]; const promise = new Promise((resolve: (value: T) => void) => { (async () => { - const request = await requests.get(`${serverURL}${api}/_preferences/${key}`, { + const request = await requests.get(`${serverURL}${api}/payload-preferences/${key}`, { headers: { 'Accept-Language': i18n.language, }, @@ -60,7 +60,7 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch const setPreference = useCallback(async (key: string, value: unknown): Promise => { preferencesRef.current[key] = value; - await requests.post(`${serverURL}${api}/_preferences/${key}`, requestOptions(value, i18n.language)); + await requests.post(`${serverURL}${api}/payload-preferences/${key}`, requestOptions(value, i18n.language)); }, [api, i18n.language, serverURL]); contextRef.current.getPreference = getPreference; diff --git a/src/auth/strategies/apiKey.ts b/src/auth/strategies/apiKey.ts index b2e1a0fe12..1f8848cd16 100644 --- a/src/auth/strategies/apiKey.ts +++ b/src/auth/strategies/apiKey.ts @@ -3,8 +3,9 @@ import crypto from 'crypto'; import { PayloadRequest } from '../../express/types'; import { Payload } from '../../payload'; import find from '../../collections/operations/find'; +import { SanitizedCollectionConfig } from '../../collections/config/types'; -export default (payload: Payload, { Model, config }): PassportAPIKey => { +export default (payload: Payload, config: SanitizedCollectionConfig): PassportAPIKey => { const { secret } = payload; const opts = { header: 'Authorization', @@ -40,7 +41,6 @@ export default (payload: Payload, { Model, config }): PassportAPIKey => { const userQuery = await find({ where, collection: { - Model, config, }, req: req as PayloadRequest, diff --git a/src/collections/initHTTP.ts b/src/collections/initHTTP.ts index 8a3206ce11..1576422695 100644 --- a/src/collections/initHTTP.ts +++ b/src/collections/initHTTP.ts @@ -17,16 +17,16 @@ export default function initCollectionsHTTP(ctx: Payload): void { router.all('*', bindCollectionMiddleware(ctx.collections[formattedCollection.slug])); if (collection.auth) { - const AuthCollection = ctx.collections[formattedCollection.slug]; + const { config } = ctx.collections[formattedCollection.slug]; if (collection.auth.useAPIKey) { - passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(ctx, AuthCollection)); + passport.use(`${config.slug}-api-key`, apiKeyStrategy(ctx, config)); } if (Array.isArray(collection.auth.strategies)) { collection.auth.strategies.forEach(({ name, strategy }, index) => { const passportStrategy = typeof strategy === 'object' ? strategy : strategy(ctx); - passport.use(`${AuthCollection.config.slug}-${name ?? index}`, passportStrategy); + passport.use(`${config.slug}-${name ?? index}`, passportStrategy); }); } } diff --git a/src/collections/operations/create.ts b/src/collections/operations/create.ts index 101f3299db..4f6da4e703 100644 --- a/src/collections/operations/create.ts +++ b/src/collections/operations/create.ts @@ -203,7 +203,7 @@ async function create( doc: resultWithLocales, payload: req.payload, password: data.password as string, - }) + }); } else { try { doc = await Model.create(resultWithLocales); diff --git a/src/collections/operations/delete.ts b/src/collections/operations/delete.ts index df59362260..26fec5fee9 100644 --- a/src/collections/operations/delete.ts +++ b/src/collections/operations/delete.ts @@ -2,7 +2,6 @@ import { Config as GeneratedTypes } from 'payload/generated-types'; import httpStatus from 'http-status'; import { AccessResult } from '../../config/types'; import { PayloadRequest } from '../../express/types'; -import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { APIError } from '../../errors'; import executeAccess from '../../auth/executeAccess'; import { BeforeOperationHook, Collection } from '../config/types'; @@ -10,6 +9,7 @@ import { Where } from '../../types'; import { afterRead } from '../../fields/hooks/afterRead'; import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions'; import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles'; +import { deleteUserPreferences } from '../../preferences/deleteUserPreferences'; import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths'; import { combineQueries } from '../../database/combineQueries'; @@ -60,7 +60,6 @@ async function deleteOperation id) }, - userCollection: collectionConfig.slug, - }); - } - preferences.Model.deleteMany({ key: { in: docs.map(({ id }) => `collection-${collectionConfig.slug}-${id}`) } }); + deleteUserPreferences({ + payload, + collectionConfig, + ids: docs.map(({ id }) => id), + }); return { docs: awaitedDocs.filter(Boolean), diff --git a/src/collections/operations/deleteByID.ts b/src/collections/operations/deleteByID.ts index d5d2137972..6e6196831f 100644 --- a/src/collections/operations/deleteByID.ts +++ b/src/collections/operations/deleteByID.ts @@ -10,6 +10,7 @@ import { afterRead } from '../../fields/hooks/afterRead'; import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions'; import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles'; import { combineQueries } from '../../database/combineQueries'; +import { deleteUserPreferences } from '../../preferences/deleteUserPreferences'; export type Arguments = { depth?: number @@ -49,7 +50,6 @@ async function deleteByID(inc payload, payload: { config, - preferences, }, }, overrideAccess, @@ -113,10 +113,11 @@ async function deleteByID(inc // Delete Preferences // ///////////////////////////////////// - if (collectionConfig.auth) { - await preferences.Model.deleteMany({ user: id, userCollection: collectionConfig.slug }); - } - await preferences.Model.deleteMany({ key: `collection-${collectionConfig.slug}-${id}` }); + deleteUserPreferences({ + payload, + collectionConfig, + ids: [id], + }); // ///////////////////////////////////// // Delete versions diff --git a/src/config/sanitize.ts b/src/config/sanitize.ts index ef054e8eac..8ce67e8198 100644 --- a/src/config/sanitize.ts +++ b/src/config/sanitize.ts @@ -7,6 +7,7 @@ import { InvalidConfiguration } from '../errors'; import sanitizeGlobals from '../globals/config/sanitize'; import checkDuplicateCollections from '../utilities/checkDuplicateCollections'; import { defaults } from './defaults'; +import getPreferencesCollection from '../preferences/preferencesCollection'; const sanitizeConfig = (config: Config): SanitizedConfig => { const sanitizedConfig = merge(defaults, config, { @@ -26,6 +27,8 @@ const sanitizeConfig = (config: Config): SanitizedConfig => { throw new InvalidConfiguration(`${sanitizedConfig.admin.user} is not a valid admin user collection`); } + sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig)); + sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig, collection)); checkDuplicateCollections(sanitizedConfig.collections); diff --git a/src/graphql/registerSchema.ts b/src/graphql/registerSchema.ts index bfd68cfce3..361b00597d 100644 --- a/src/graphql/registerSchema.ts +++ b/src/graphql/registerSchema.ts @@ -7,7 +7,6 @@ import buildLocaleInputType from './schema/buildLocaleInputType'; import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType'; import initCollections from '../collections/graphql/init'; import initGlobals from '../globals/graphql/init'; -import initPreferences from '../preferences/graphql/init'; import buildPoliciesType from './schema/buildPoliciesType'; import accessResolver from '../auth/graphql/resolvers/access'; @@ -37,7 +36,6 @@ export default function registerGraphQLSchema(payload: Payload): void { initCollections(payload); initGlobals(payload); - initPreferences(payload); payload.Query.fields.Access = { type: buildPoliciesType(payload), diff --git a/src/initHTTP.ts b/src/initHTTP.ts index 0872e01aaf..0117b1b580 100644 --- a/src/initHTTP.ts +++ b/src/initHTTP.ts @@ -8,7 +8,6 @@ import initAdmin from './express/admin'; import initAuth from './auth/init'; import access from './auth/requestHandlers/access'; import initCollectionsHTTP from './collections/initHTTP'; -import initPreferences from './preferences/init'; import initGlobalsHTTP from './globals/initHTTP'; import initGraphQLPlayground from './graphql/initPlayground'; import initStatic from './express/static'; @@ -49,7 +48,6 @@ export const initHTTP = async (options: InitOptions): Promise => { } initAdmin(payload); - initPreferences(payload); payload.router.get('/access', access); diff --git a/src/payload.ts b/src/payload.ts index de8bed6c94..d8f0918f65 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -19,7 +19,6 @@ import localOperations from './collections/operations/local'; import localGlobalOperations from './globals/operations/local'; import { decrypt, encrypt } from './auth/crypto'; import { BuildEmailResult } from './email/types'; -import { Preferences } from './preferences/types'; import { Options as CreateOptions } from './collections/operations/local/create'; import { Options as FindOptions } from './collections/operations/local/find'; @@ -57,7 +56,6 @@ import sendEmail from './email/sendEmail'; import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit'; import Logger from './utilities/logger'; -import PreferencesModel from './preferences/model'; import findConfig from './config/find'; import { defaults as emailDefaults } from './email/defaults'; @@ -80,8 +78,6 @@ export class BasePayload { [slug: string]: CollectionModel; } = {}; - preferences: Preferences; - globals: Globals; logger: pino.Logger; @@ -239,7 +235,6 @@ export class BasePayload { if (this.db.init) { await this.db?.init({ payload: this, config: this.config }); } - this.preferences = { Model: PreferencesModel }; serverInitTelemetry(this); diff --git a/src/preferences/deleteUserPreferences.ts b/src/preferences/deleteUserPreferences.ts new file mode 100644 index 0000000000..5c35d97367 --- /dev/null +++ b/src/preferences/deleteUserPreferences.ts @@ -0,0 +1,20 @@ +import type { Payload } from '../index'; +import type { SanitizedCollectionConfig } from '../collections/config/types'; + +type Args = { + payload: Payload + /** + * User IDs to delete + */ + ids: (string|number)[] + collectionConfig: SanitizedCollectionConfig +} +export const deleteUserPreferences = ({ payload, ids, collectionConfig }: Args) => { + if (collectionConfig.auth) { + payload.collections['payload-preferences'].Model.deleteMany({ + user: { in: ids }, + userCollection: collectionConfig.slug, + }); + } + payload.collections['payload-preferences'].Model.deleteMany({ key: { in: ids.map((id) => `collection-${collectionConfig.slug}-${id}`) } }); +}; diff --git a/src/preferences/graphql/init.ts b/src/preferences/graphql/init.ts deleted file mode 100644 index 071e553980..0000000000 --- a/src/preferences/graphql/init.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { GraphQLJSON } from 'graphql-type-json'; -import { - GraphQLNonNull, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; -import { DateTimeResolver } from 'graphql-scalars'; -import findOne from '../operations/findOne'; -import update from '../operations/update'; -import deleteOperation from '../operations/delete'; -import { Payload } from '../../payload'; - -function initCollectionsGraphQL(payload: Payload): void { - const valueType = GraphQLJSON; - - const preferenceType = new GraphQLObjectType({ - name: 'Preference', - fields: { - key: { - type: new GraphQLNonNull(GraphQLString), - }, - value: { type: valueType }, - createdAt: { type: new GraphQLNonNull(DateTimeResolver) }, - updatedAt: { type: new GraphQLNonNull(DateTimeResolver) }, - }, - }); - - payload.Query.fields.Preference = { - type: preferenceType, - args: { - key: { type: GraphQLString }, - }, - resolve: (_, { key }, context) => { - const { user } = context.req; - return findOne({ key, user, req: context.req }); - }, - }; - - payload.Mutation.fields.updatePreference = { - type: preferenceType, - args: { - key: { type: new GraphQLNonNull(GraphQLString) }, - value: { type: valueType }, - }, - resolve: (_, { key, value }, context) => { - const { user } = context.req; - return update({ key, user, req: context.req, value }); - }, - }; - - payload.Mutation.fields.deletePreference = { - type: preferenceType, - args: { - key: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: (_, { key }, context) => { - const { user } = context.req; - return deleteOperation({ key, user, req: context.req }); - }, - }; -} - -export default initCollectionsGraphQL; diff --git a/src/preferences/init.ts b/src/preferences/init.ts deleted file mode 100644 index 022390fc1d..0000000000 --- a/src/preferences/init.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express from 'express'; -import findOne from './requestHandlers/findOne'; -import update from './requestHandlers/update'; -import deleteHandler from './requestHandlers/delete'; -import { Payload } from '../payload'; - -export default function initPreferences(ctx: Payload): void { - if (!ctx.local) { - const router = express.Router(); - router - .route('/_preferences/:key') - .get(findOne) - .post(update) - .delete(deleteHandler); - - ctx.router.use(router); - } -} diff --git a/src/preferences/model.ts b/src/preferences/model.ts deleted file mode 100644 index a9b2280099..0000000000 --- a/src/preferences/model.ts +++ /dev/null @@ -1,15 +0,0 @@ -import mongoose, { Schema } from 'mongoose'; -import { Preference } from './types'; - -const Model = mongoose.model('_preferences', new Schema({ - user: { - type: Schema.Types.ObjectId, - refPath: 'userCollection', - }, - userCollection: String, - key: String, - value: Schema.Types.Mixed, -}, { timestamps: true }) - .index({ user: 1, key: 1, userCollection: 1 }, { unique: true })); - -export default Model; diff --git a/src/preferences/operations/delete.ts b/src/preferences/operations/delete.ts index 231cd4aee5..719e972f68 100644 --- a/src/preferences/operations/delete.ts +++ b/src/preferences/operations/delete.ts @@ -1,19 +1,16 @@ import executeAccess from '../../auth/executeAccess'; import defaultAccess from '../../auth/defaultAccess'; -import { Document } from '../../types'; +import { Document, Where } from '../../types'; import UnauthorizedError from '../../errors/UnathorizedError'; import { PreferenceRequest } from '../types'; +import NotFound from '../../errors/NotFound'; async function deleteOperation(args: PreferenceRequest): Promise { const { overrideAccess, req, req: { - payload: { - preferences: { - Model, - }, - }, + payload, }, user, key, @@ -27,15 +24,25 @@ async function deleteOperation(args: PreferenceRequest): Promise { await executeAccess({ req }, defaultAccess); } - const filter = { - key, - user: user.id, - userCollection: user.collection, + const where: Where = { + and: [ + { key: { equals: key } }, + { 'user.value': { equals: user.id } }, + { 'user.relationTo': { equals: user.collection } }, + ], }; - const result = await Model.findOneAndDelete(filter); + const result = await payload.delete({ + collection: 'payload-preferences', + where, + depth: 0, + user, + }); - return result; + if (result.docs.length === 1) { + return result.docs[0]; + } + throw new NotFound(); } export default deleteOperation; diff --git a/src/preferences/operations/findOne.ts b/src/preferences/operations/findOne.ts index b1461309aa..3f2573b73d 100644 --- a/src/preferences/operations/findOne.ts +++ b/src/preferences/operations/findOne.ts @@ -1,40 +1,34 @@ -import { Preference, PreferenceRequest } from '../types'; -import executeAccess from '../../auth/executeAccess'; -import defaultAccess from '../../auth/defaultAccess'; -import UnauthorizedError from '../../errors/UnathorizedError'; +import { Config as GeneratedTypes } from 'payload/generated-types'; +import { PreferenceRequest } from '../types'; +import { Where } from '../../types'; -async function findOne(args: PreferenceRequest): Promise { +async function findOne(args: PreferenceRequest): Promise { const { - overrideAccess, - req, req: { - payload: { - preferences: { Model }, - }, + payload, }, user, key, } = args; - if (!user) { - throw new UnauthorizedError(req.t); - } - - if (!overrideAccess) { - await executeAccess({ req }, defaultAccess); - } - - const filter = { - key, - user: user.id, - userCollection: user.collection, + const where: Where = { + and: [ + { key: { equals: key } }, + { 'user.value': { equals: user.id } }, + { 'user.relationTo': { equals: user.collection } }, + ], }; - const doc = await Model.findOne(filter); + const { docs } = await payload.find({ + collection: 'payload-preferences', + where, + depth: 0, + user, + }); - if (!doc) return null; + if (docs.length === 0) return null; - return doc; + return docs[0]; } export default findOne; diff --git a/src/preferences/operations/update.ts b/src/preferences/operations/update.ts index 5359fe2af2..1483e6ddcc 100644 --- a/src/preferences/operations/update.ts +++ b/src/preferences/operations/update.ts @@ -1,4 +1,4 @@ -import { Preference, PreferenceUpdateRequest } from '../types'; +import { PreferenceUpdateRequest } from '../types'; import defaultAccess from '../../auth/defaultAccess'; import executeAccess from '../../auth/executeAccess'; import UnauthorizedError from '../../errors/UnathorizedError'; @@ -9,16 +9,27 @@ async function update(args: PreferenceUpdateRequest) { user, req, req: { - payload: { - preferences: { - Model, - }, - }, + payload, }, key, value, } = args; + const collection = 'payload-preferences'; + + const filter = { + key, + user: { + value: user.id, + relationTo: user.collection, + }, + }; + + const preference = { + ...filter, + value, + }; + if (!user) { throw new UnauthorizedError(req.t); } @@ -27,10 +38,16 @@ async function update(args: PreferenceUpdateRequest) { await executeAccess({ req }, defaultAccess); } - const filter = { user: user.id, key, userCollection: user.collection }; - const preference: Preference = { ...filter, value }; - await Model.updateOne(filter, { ...preference }, { upsert: true }); - + const { Model } = payload.collections[collection]; + const updateResult = await Model.updateOne(filter, preference); + if (updateResult.modifiedCount === 0) { + // TODO: workaround to prevent race-conditions 500 errors from violating unique constraints + try { + await Model.create(preference); + } catch (err: unknown) { + await Model.updateOne(filter, preference); + } + } return preference; } diff --git a/src/preferences/preferencesCollection.ts b/src/preferences/preferencesCollection.ts new file mode 100644 index 0000000000..f987d90b1d --- /dev/null +++ b/src/preferences/preferencesCollection.ts @@ -0,0 +1,84 @@ +import { CollectionConfig } from '../collections/config/types'; +import { Access, Config } from '../config/types'; +import findOne from './requestHandlers/findOne'; +import update from './requestHandlers/update'; +import deleteHandler from './requestHandlers/delete'; + +const preferenceAccess: Access = ({ req }) => ({ + 'user.value': { + equals: req?.user?.id, + }, +}); + +const getPreferencesCollection = (config: Config): CollectionConfig => ({ + slug: 'payload-preferences', + admin: { + hidden: true, + }, + access: { + read: preferenceAccess, + delete: preferenceAccess, + }, + fields: [ + { + name: 'user', + type: 'relationship', + relationTo: config.collections + .filter((collectionConfig) => collectionConfig.auth) + .map((collectionConfig) => collectionConfig.slug), + required: true, + hooks: { + beforeValidate: [ + (({ req }) => { + if (!req?.user) { + return null; + } + return { + value: req?.user.id, + relationTo: req?.user.collection, + }; + }), + ], + }, + }, + { + name: 'key', + type: 'text', + }, + { + name: 'value', + type: 'json', + }, + ], + indexes: [ + { + fields: { + 'user.value': 1, + 'user.relationTo': 1, + key: 1, + }, + options: { + unique: true, + }, + }, + ], + endpoints: [ + { + method: 'get', + path: '/:key', + handler: findOne, + }, + { + method: 'delete', + path: '/:key', + handler: deleteHandler, + }, + { + method: 'post', + path: '/:key', + handler: update, + }, + ], +}); + +export default getPreferencesCollection; diff --git a/src/preferences/requestHandlers/delete.ts b/src/preferences/requestHandlers/delete.ts index aee7f56f8b..10bb8e2b6f 100644 --- a/src/preferences/requestHandlers/delete.ts +++ b/src/preferences/requestHandlers/delete.ts @@ -4,7 +4,7 @@ import { PayloadRequest } from '../../express/types'; import formatSuccessResponse from '../../express/responses/formatSuccess'; import deleteOperation from '../operations/delete'; -export default async function deleteHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { +export default async function deleteHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { try { await deleteOperation({ req, diff --git a/src/preferences/requestHandlers/findOne.ts b/src/preferences/requestHandlers/findOne.ts index 8ae683e50b..a14e450b5e 100644 --- a/src/preferences/requestHandlers/findOne.ts +++ b/src/preferences/requestHandlers/findOne.ts @@ -1,10 +1,10 @@ import { NextFunction, Response } from 'express'; import httpStatus from 'http-status'; +import { Config as GeneratedTypes } from 'payload/generated-types'; import { PayloadRequest } from '../../express/types'; -import { Preference } from '../types'; import findOne from '../operations/findOne'; -export default async function findOneHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { +export default async function findOneHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { try { const result = await findOne({ req, diff --git a/src/preferences/requestHandlers/update.ts b/src/preferences/requestHandlers/update.ts index 3fb75eece9..abd24f7215 100644 --- a/src/preferences/requestHandlers/update.ts +++ b/src/preferences/requestHandlers/update.ts @@ -1,13 +1,11 @@ import { NextFunction, Response } from 'express'; import httpStatus from 'http-status'; +import { Config as GeneratedTypes } from 'payload/generated-types'; import { PayloadRequest } from '../../express/types'; import formatSuccessResponse from '../../express/responses/formatSuccess'; import update from '../operations/update'; -export type UpdatePreferenceResult = Promise | void>; -export type UpdatePreferenceResponse = (req: PayloadRequest, res: Response, next: NextFunction) => UpdatePreferenceResult; - -export default async function updateHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { +export default async function updateHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { try { const doc = await update({ req, diff --git a/src/preferences/types.ts b/src/preferences/types.ts index d4a52dfc69..45caa7b781 100644 --- a/src/preferences/types.ts +++ b/src/preferences/types.ts @@ -1,20 +1,6 @@ -import { Model } from 'mongoose'; import { User } from '../auth'; import { PayloadRequest } from '../express/types'; -export type Preference = { - user: string; - userCollection: string; - key: string; - value: { [key: string]: unknown } | unknown; - createdAt?: Date; - updatedAt?: Date; -}; - -export type Preferences = { - Model: Model -} - export type PreferenceRequest = { overrideAccess?: boolean; req: PayloadRequest;