22
demo/collections/CustomID.ts
Normal file
22
demo/collections/CustomID.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CollectionConfig } from '../../src/collections/config/types';
|
||||
|
||||
const CustomID: CollectionConfig = {
|
||||
slug: 'custom-id',
|
||||
labels: {
|
||||
singular: 'CustomID',
|
||||
plural: 'CustomIDs',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default CustomID;
|
||||
@@ -29,7 +29,7 @@ const RelationshipA: CollectionConfig = {
|
||||
name: 'postLocalizedMultiple',
|
||||
label: 'Localized Post Multiple',
|
||||
type: 'relationship',
|
||||
relationTo: ['localized-posts', 'all-fields'],
|
||||
relationTo: ['localized-posts', 'all-fields', 'custom-id'],
|
||||
hasMany: true,
|
||||
localized: true,
|
||||
},
|
||||
@@ -49,6 +49,14 @@ const RelationshipA: CollectionConfig = {
|
||||
relationTo: 'relationship-b',
|
||||
hasMany: false,
|
||||
},
|
||||
{
|
||||
name: 'customID',
|
||||
label: 'CustomID Relation',
|
||||
type: 'relationship',
|
||||
relationTo: 'custom-id',
|
||||
hasMany: true,
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import Conditions from './collections/Conditions';
|
||||
import CustomComponents from './collections/CustomComponents';
|
||||
import File from './collections/File';
|
||||
import Blocks from './collections/Blocks';
|
||||
import CustomID from './collections/CustomID';
|
||||
import DefaultValues from './collections/DefaultValues';
|
||||
import HiddenFields from './collections/HiddenFields';
|
||||
import Hooks from './collections/Hooks';
|
||||
@@ -64,6 +65,7 @@ export default buildConfig({
|
||||
Code,
|
||||
Conditions,
|
||||
CustomComponents,
|
||||
CustomID,
|
||||
File,
|
||||
DefaultValues,
|
||||
Blocks,
|
||||
|
||||
@@ -84,6 +84,24 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
### Customizable ID
|
||||
|
||||
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
|
||||
Users are then required to provide a custom ID value when creating a record through the Admin UI or API.
|
||||
Valid ID types are `number` and `text`.
|
||||
|
||||
Example:
|
||||
```js
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Admin config
|
||||
|
||||
In addition to each field's base configuration, you can define specific traits and properties for fields that only have effect on how they are rendered in the Admin panel. The following properties are available for all fields within the `admin` property:
|
||||
|
||||
@@ -8,6 +8,7 @@ import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
|
||||
import ListControls from '../../../../elements/ListControls';
|
||||
import Paginator from '../../../../elements/Paginator';
|
||||
import UploadGallery from '../../../../elements/UploadGallery';
|
||||
import { Field } from '../../../../../../fields/config/types';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
@@ -45,7 +46,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
|
||||
|
||||
useEffect(() => {
|
||||
setFields(formatFields(collection));
|
||||
setFields(formatFields(collection) as Field[]);
|
||||
}, [collection]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { Field } from '../../../../../fields/config/types';
|
||||
|
||||
const formatFields = (collection: SanitizedCollectionConfig, isEditing: boolean): Field[] => (isEditing
|
||||
? collection.fields.filter(({ name }) => name !== 'id')
|
||||
: collection.fields);
|
||||
|
||||
export default formatFields;
|
||||
@@ -7,6 +7,7 @@ import usePayloadAPI from '../../../../hooks/usePayloadAPI';
|
||||
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
|
||||
import { DocumentInfoProvider } from '../../../utilities/DocumentInfo';
|
||||
import DefaultEdit from './Default';
|
||||
import formatFields from './formatFields';
|
||||
import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema';
|
||||
import { NegativeFieldGutterProvider } from '../../../forms/FieldTypeGutter/context';
|
||||
import { useLocale } from '../../../utilities/Locale';
|
||||
@@ -29,8 +30,8 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
} = {},
|
||||
} = {},
|
||||
} = {},
|
||||
fields,
|
||||
} = collection;
|
||||
const [fields] = useState(() => formatFields(collection, isEditing));
|
||||
|
||||
const locale = useLocale();
|
||||
const { serverURL, routes: { admin, api } } = useConfig();
|
||||
@@ -110,7 +111,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
componentProps={{
|
||||
isLoading,
|
||||
data: dataToRender,
|
||||
collection,
|
||||
collection: { ...collection, fields },
|
||||
permissions: collectionPermissions,
|
||||
isEditing,
|
||||
onSave,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const formatFields = (config) => {
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { Field } from '../../../../../fields/config/types';
|
||||
|
||||
const formatFields = (config: SanitizedCollectionConfig): (Field | { name: string, label: string, type: string })[] => {
|
||||
const hasID = config.fields.findIndex(({ name }) => name === 'id') > -1;
|
||||
let fields = config.fields.reduce((formatted, field) => {
|
||||
if (field.hidden === true || field?.admin?.disabled === true) {
|
||||
return formatted;
|
||||
@@ -8,7 +12,7 @@ const formatFields = (config) => {
|
||||
...formatted,
|
||||
field,
|
||||
];
|
||||
}, [{ name: 'id', label: 'ID', type: 'text' }]);
|
||||
}, hasID ? [] : [{ name: 'id', label: 'ID', type: 'text' }]);
|
||||
|
||||
if (config.timestamps) {
|
||||
fields = fields.concat([
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { DateTimeResolver } from 'graphql-scalars';
|
||||
import { GraphQLString, GraphQLObjectType, GraphQLBoolean, GraphQLNonNull, GraphQLInt } from 'graphql';
|
||||
import {
|
||||
GraphQLString,
|
||||
GraphQLObjectType,
|
||||
GraphQLBoolean,
|
||||
GraphQLNonNull,
|
||||
GraphQLInt,
|
||||
} from 'graphql';
|
||||
|
||||
import formatName from '../../graphql/utilities/formatName';
|
||||
import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType';
|
||||
import { BaseFields } from './types';
|
||||
import { getCollectionIDType } from '../../graphql/schema/buildMutationInputType';
|
||||
|
||||
function registerCollections(): void {
|
||||
const {
|
||||
@@ -22,13 +29,11 @@ function registerCollections(): void {
|
||||
singular,
|
||||
plural,
|
||||
},
|
||||
fields: initialFields,
|
||||
fields,
|
||||
timestamps,
|
||||
},
|
||||
} = collection;
|
||||
|
||||
const fields = [...initialFields];
|
||||
|
||||
const singularLabel = formatName(singular);
|
||||
let pluralLabel = formatName(plural);
|
||||
|
||||
@@ -43,16 +48,23 @@ function registerCollections(): void {
|
||||
|
||||
collection.graphQL = {};
|
||||
|
||||
const baseFields: BaseFields = {
|
||||
id: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
},
|
||||
};
|
||||
const idField = fields.find(({ name }) => name === 'id');
|
||||
const idType = getCollectionIDType(collection.config);
|
||||
|
||||
const baseFields: BaseFields = {};
|
||||
|
||||
const whereInputFields = [
|
||||
...fields,
|
||||
];
|
||||
|
||||
if (!idField) {
|
||||
baseFields.id = { type: idType };
|
||||
whereInputFields.push({
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
if (timestamps) {
|
||||
baseFields.createdAt = {
|
||||
type: new GraphQLNonNull(DateTimeResolver),
|
||||
@@ -105,7 +117,7 @@ function registerCollections(): void {
|
||||
|
||||
collection.graphQL.updateMutationInputType = new GraphQLNonNull(this.buildMutationInputType(
|
||||
`${singularLabel}Update`,
|
||||
fields,
|
||||
fields.filter((field) => field.name !== 'id'),
|
||||
`${singularLabel}Update`,
|
||||
true,
|
||||
));
|
||||
@@ -113,7 +125,7 @@ function registerCollections(): void {
|
||||
this.Query.fields[singularLabel] = {
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: GraphQLString },
|
||||
id: { type: idType },
|
||||
...(this.config.localization ? {
|
||||
locale: { type: this.types.localeInputType },
|
||||
fallbackLocale: { type: this.types.fallbackLocaleInputType },
|
||||
@@ -148,7 +160,7 @@ function registerCollections(): void {
|
||||
this.Mutation.fields[`update${singularLabel}`] = {
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: new GraphQLNonNull(GraphQLString) },
|
||||
id: { type: new GraphQLNonNull(idType) },
|
||||
data: { type: collection.graphQL.updateMutationInputType },
|
||||
},
|
||||
resolve: update(collection),
|
||||
@@ -157,7 +169,7 @@ function registerCollections(): void {
|
||||
this.Mutation.fields[`delete${singularLabel}`] = {
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: new GraphQLNonNull(GraphQLString) },
|
||||
id: { type: new GraphQLNonNull(idType) },
|
||||
},
|
||||
resolve: deleteResolver(collection),
|
||||
};
|
||||
|
||||
@@ -279,4 +279,139 @@ describe('GrahpQL Resolvers', () => {
|
||||
expect(typeof error.response.errors[0].message).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom ID', () => {
|
||||
it('should create', async () => {
|
||||
const id = 10;
|
||||
const query = `mutation {
|
||||
createCustomID(data: {
|
||||
id: ${id},
|
||||
name: "custom"
|
||||
}) {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}`;
|
||||
const response = await client.request(query);
|
||||
const data = response.createCustomID;
|
||||
expect(data.id).toStrictEqual(id);
|
||||
});
|
||||
|
||||
it('should update', async () => {
|
||||
const id = 11;
|
||||
const name = 'custom name';
|
||||
|
||||
const query = `
|
||||
mutation {
|
||||
createCustomID(data: {
|
||||
id: ${id},
|
||||
name: "${name}"
|
||||
}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(query);
|
||||
const updatedName = 'updated name';
|
||||
|
||||
const update = `
|
||||
mutation {
|
||||
updateCustomID(id: ${id} data: {name: "${updatedName}"}) {
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await client.request(update);
|
||||
const data = response.updateCustomID;
|
||||
|
||||
expect(data.name).toStrictEqual(updatedName);
|
||||
expect(data.name).not.toStrictEqual(name);
|
||||
});
|
||||
|
||||
it('should query on id', async () => {
|
||||
const id = 15;
|
||||
const name = 'custom name';
|
||||
|
||||
const create = `mutation {
|
||||
createCustomID(data: {
|
||||
id: ${id},
|
||||
name: "${name}"
|
||||
}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(create);
|
||||
|
||||
const query = `
|
||||
query {
|
||||
CustomIDs(where: { id: { equals: ${id} } }) {
|
||||
docs {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const response = await client.request(query);
|
||||
const [doc] = response.CustomIDs.docs;
|
||||
expect(doc.id).toStrictEqual(id);
|
||||
expect(doc.name).toStrictEqual(name);
|
||||
});
|
||||
|
||||
it('should delete', async () => {
|
||||
const id = 12;
|
||||
const query = `mutation {
|
||||
createCustomID(data: {
|
||||
id: ${id},
|
||||
name: "delete me"
|
||||
}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(query);
|
||||
|
||||
const deleteMutation = `mutation {
|
||||
deleteCustomID(id: ${id}) {
|
||||
id
|
||||
}
|
||||
}`;
|
||||
const deleteResponse = await client.request(deleteMutation);
|
||||
const deletedId = deleteResponse.deleteCustomID.id;
|
||||
|
||||
expect(deletedId).toStrictEqual(id);
|
||||
});
|
||||
|
||||
it('should allow relationships', async () => {
|
||||
const id = 13;
|
||||
const query = `mutation {
|
||||
createCustomID(data: {
|
||||
id: ${id},
|
||||
name: "relate me"
|
||||
}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(query);
|
||||
const relation = `mutation {
|
||||
createRelationshipA(data: {
|
||||
customID: [ ${id} ]
|
||||
}) {
|
||||
customID {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const relationResponse = await client.request(relation);
|
||||
const { customID } = relationResponse.createRelationshipA;
|
||||
|
||||
expect(customID).toHaveLength(1);
|
||||
expect(customID).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,18 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
|
||||
await executeAccess({ req }, collectionConfig.access.create);
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Custom id
|
||||
// /////////////////////////////////////
|
||||
|
||||
const hasIdField = collectionConfig.fields.findIndex(({ name }) => name === 'id') > -1;
|
||||
if (hasIdField) {
|
||||
data = {
|
||||
_id: data.id,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Upload and resize potential files
|
||||
// /////////////////////////////////////
|
||||
@@ -218,6 +230,8 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
|
||||
let result: Document = doc.toJSON({ virtuals: true });
|
||||
const verificationToken = result._verificationToken;
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id;
|
||||
result = JSON.stringify(result);
|
||||
result = JSON.parse(result);
|
||||
result = sanitizeInternalFields(result);
|
||||
|
||||
@@ -135,6 +135,8 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
|
||||
|
||||
let result: Document = doc.toJSON({ virtuals: true });
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id;
|
||||
result = JSON.stringify(result);
|
||||
result = JSON.parse(result);
|
||||
result = sanitizeInternalFields(result);
|
||||
|
||||
@@ -100,6 +100,8 @@ async function find(incomingArgs: Arguments): Promise<PaginatedDocs> {
|
||||
} else {
|
||||
sort = '-_id';
|
||||
}
|
||||
} else if (sort === 'id' || sort === '-id') {
|
||||
sort = sort.replace('id', '_id');
|
||||
}
|
||||
|
||||
const optionsToExecute = {
|
||||
|
||||
@@ -265,6 +265,9 @@ async function update(incomingArgs: Arguments): Promise<Document> {
|
||||
}
|
||||
|
||||
result = result.toJSON({ virtuals: true });
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id;
|
||||
result = JSON.stringify(result);
|
||||
result = JSON.parse(result);
|
||||
result = sanitizeInternalFields(result);
|
||||
|
||||
@@ -548,4 +548,92 @@ describe('Collections - REST', () => {
|
||||
expect(failedResponse.status).toStrictEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom ID', () => {
|
||||
const document = {
|
||||
id: 1,
|
||||
name: 'name',
|
||||
};
|
||||
let data;
|
||||
beforeAll(async (done) => {
|
||||
// create document
|
||||
const create = await fetch(`${url}/api/custom-id`, {
|
||||
body: JSON.stringify(document),
|
||||
headers,
|
||||
method: 'post',
|
||||
});
|
||||
data = await create.json();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('should create collections with custom ID', async () => {
|
||||
expect(data.doc.id).toBe(document.id);
|
||||
});
|
||||
|
||||
it('should read collections by custom ID', async () => {
|
||||
const response = await fetch(`${url}/api/custom-id/${document.id}`, {
|
||||
headers,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
expect(result.id).toStrictEqual(document.id);
|
||||
expect(result.name).toStrictEqual(document.name);
|
||||
});
|
||||
|
||||
it('should update collection by custom ID', async () => {
|
||||
const updatedDoc = { id: 'cannot-update-id', name: 'updated' };
|
||||
const response = await fetch(`${url}/api/custom-id/${document.id}`, {
|
||||
headers,
|
||||
body: JSON.stringify(updatedDoc),
|
||||
method: 'put',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
expect(result.doc.id).not.toStrictEqual(updatedDoc.id);
|
||||
expect(result.doc.name).not.toStrictEqual(document.name);
|
||||
expect(result.doc.name).toStrictEqual(updatedDoc.name);
|
||||
});
|
||||
|
||||
it('should delete collection by custom ID', async () => {
|
||||
const doc = {
|
||||
id: 2,
|
||||
name: 'delete me',
|
||||
};
|
||||
const createResponse = await fetch(`${url}/api/custom-id`, {
|
||||
body: JSON.stringify(doc),
|
||||
headers,
|
||||
method: 'post',
|
||||
});
|
||||
const result = await createResponse.json();
|
||||
const response = await fetch(`${url}/api/custom-id/${result.doc.id}`, {
|
||||
headers,
|
||||
method: 'delete',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const deleteData = await response.json();
|
||||
expect(deleteData.id).toBe(doc.id);
|
||||
});
|
||||
|
||||
it('should allow querying by custom ID', async () => {
|
||||
const response = await fetch(`${url}/api/custom-id?where[id][equals]=${document.id}`, {
|
||||
headers,
|
||||
method: 'get',
|
||||
});
|
||||
const emptyResponse = await fetch(`${url}/api/custom-id?where[id][equals]=900`, {
|
||||
headers,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const emptyResult = await emptyResponse.json();
|
||||
|
||||
expect(result.docs).toHaveLength(1);
|
||||
expect(emptyResult.docs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,5 +103,59 @@ describe('Collections - REST', () => {
|
||||
expect(doc.postMaxDepth).toBe(documentB.id);
|
||||
expect(doc.postMaxDepth).not.toHaveProperty('post');
|
||||
});
|
||||
|
||||
it('should allow a custom id relation', async () => {
|
||||
const customID = {
|
||||
id: 30,
|
||||
name: 'custom',
|
||||
};
|
||||
|
||||
const newCustomID = await fetch(`${url}/api/custom-id`, {
|
||||
headers,
|
||||
body: JSON.stringify(customID),
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const custom = await newCustomID.json();
|
||||
const response = await fetch(`${url}/api/relationship-a/${documentA.id}`, {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...documentA,
|
||||
post: documentB.id,
|
||||
customID: [custom.doc.id],
|
||||
}),
|
||||
method: 'put',
|
||||
});
|
||||
const { doc } = await response.json();
|
||||
expect(doc.customID[0].id).toBe(customID.id);
|
||||
});
|
||||
|
||||
it('should allow a custom id relation and parse the id type', async () => {
|
||||
const customID = {
|
||||
id: '40',
|
||||
name: 'custom',
|
||||
};
|
||||
|
||||
const newCustomID = await fetch(`${url}/api/custom-id`, {
|
||||
headers,
|
||||
body: JSON.stringify(customID),
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const custom = await newCustomID.json();
|
||||
const response = await fetch(`${url}/api/relationship-a/${documentA.id}`, {
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...documentA,
|
||||
post: documentB.id,
|
||||
customID: [custom.doc.id],
|
||||
}),
|
||||
method: 'put',
|
||||
});
|
||||
const { doc } = await response.json();
|
||||
|
||||
expect(custom.doc.id).toBe(parseFloat(customID.id));
|
||||
expect(doc.customID[0].id).toBe(parseFloat(customID.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ValidationResult } from 'joi';
|
||||
import schema from './schema';
|
||||
import collectionSchema from '../collections/config/schema';
|
||||
import Logger from '../utilities/logger';
|
||||
import { SanitizedConfig } from './types';
|
||||
import { SanitizedCollectionConfig } from '../collections/config/types';
|
||||
import fieldSchema from '../fields/config/schema';
|
||||
import fieldSchema, { idField } from '../fields/config/schema';
|
||||
import { SanitizedGlobalConfig } from '../globals/config/types';
|
||||
import globalSchema from '../globals/config/schema';
|
||||
|
||||
@@ -12,7 +13,17 @@ const logger = Logger();
|
||||
const validateFields = (context: string, entity: SanitizedCollectionConfig | SanitizedGlobalConfig): string[] => {
|
||||
const errors: string[] = [];
|
||||
entity.fields.forEach((field) => {
|
||||
let idResult: Partial<ValidationResult> = { error: null };
|
||||
if (field.name === 'id') {
|
||||
idResult = idField.validate(field, { abortEarly: false });
|
||||
}
|
||||
|
||||
const result = fieldSchema.validate(field, { abortEarly: false });
|
||||
if (idResult.error) {
|
||||
idResult.error.details.forEach(({ message }) => {
|
||||
errors.push(`${context} "${entity.slug}" > Field "${field.name}" > ${message}`);
|
||||
});
|
||||
}
|
||||
if (result.error) {
|
||||
result.error.details.forEach(({ message }) => {
|
||||
errors.push(`${context} "${entity.slug}" > Field "${field.name}" > ${message}`);
|
||||
|
||||
@@ -47,6 +47,13 @@ export const baseField = joi.object().keys({
|
||||
admin: baseAdminFields.default(),
|
||||
}).default();
|
||||
|
||||
export const idField = baseField.keys({
|
||||
name: joi.string().valid('id'),
|
||||
type: joi.string().valid('text', 'number'),
|
||||
required: joi.not(false, 0).default(true),
|
||||
localized: joi.invalid(true),
|
||||
});
|
||||
|
||||
export const text = baseField.keys({
|
||||
type: joi.string().valid('text').required(),
|
||||
name: joi.string().required(),
|
||||
|
||||
@@ -87,10 +87,14 @@ const traverseFields = (args: Arguments): void => {
|
||||
|
||||
if ((field.type === 'upload' || field.type === 'relationship')
|
||||
&& (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
|
||||
dataCopy[field.name] = null;
|
||||
if (field.type === 'relationship' && field.hasMany === true) {
|
||||
dataCopy[field.name] = [];
|
||||
} else {
|
||||
dataCopy[field.name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' && field.hasMany && (data[field.name]?.[0] === '' || data[field.name]?.[0] === 'none' || data[field.name]?.[0] === 'null')) {
|
||||
if (field.type === 'relationship' && field.hasMany && (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
|
||||
dataCopy[field.name] = [];
|
||||
}
|
||||
|
||||
@@ -98,6 +102,15 @@ const traverseFields = (args: Arguments): void => {
|
||||
dataCopy[field.name] = parseFloat(data[field.name]);
|
||||
}
|
||||
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof data[field.name] === 'string') {
|
||||
dataCopy[field.name] = parseFloat(data[field.name]);
|
||||
}
|
||||
if (field.type === 'text' && typeof data[field.name]?.toString === 'function' && typeof data[field.name] !== 'string') {
|
||||
dataCopy[field.name] = dataCopy[field.name].toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
if (data[field.name] === 'true') dataCopy[field.name] = true;
|
||||
if (data[field.name] === 'false') dataCopy[field.name] = false;
|
||||
@@ -267,6 +280,44 @@ const traverseFields = (args: Arguments): void => {
|
||||
updatedData[field.name] = field.defaultValue;
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(dataCopy[field.name])) {
|
||||
dataCopy[field.name].forEach((relatedDoc: {value: unknown, relationTo: string}, i) => {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === relatedDoc.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name][i] = { ...relatedDoc, value: parseFloat(relatedDoc.value as string) };
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]?.relationTo) {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === dataCopy[field.name].relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name] = { ...dataCopy[field.name], value: parseFloat(dataCopy[field.name].value as string) };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(dataCopy[field.name])) {
|
||||
dataCopy[field.name].forEach((relatedDoc: unknown, i) => {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name][i] = parseFloat(relatedDoc as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]) {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name] = parseFloat(dataCopy[field.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'point' && data[field.name]) {
|
||||
transformActions.push(() => {
|
||||
if (Array.isArray(data[field.name]) && data[field.name][0] !== null && data[field.name][1] !== null) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
GraphQLEnumType,
|
||||
GraphQLFloat,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLScalarType,
|
||||
@@ -16,10 +17,26 @@ import formatName from '../utilities/formatName';
|
||||
import combineParentName from '../utilities/combineParentName';
|
||||
import { ArrayField, Field, FieldWithSubFields, GroupField, RelationshipField, RowField, SelectField } from '../../fields/config/types';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
import payload from '../../index';
|
||||
import { SanitizedCollectionConfig } from '../../collections/config/types';
|
||||
|
||||
export const getCollectionIDType = (config: SanitizedCollectionConfig): GraphQLScalarType => {
|
||||
const idField = config.fields.find(({ name }) => name === 'id');
|
||||
if (!idField) return GraphQLString;
|
||||
switch (idField.type) {
|
||||
case 'number':
|
||||
return GraphQLInt;
|
||||
default:
|
||||
return GraphQLString;
|
||||
}
|
||||
};
|
||||
|
||||
function buildMutationInputType(name: string, fields: Field[], parentName: string, forceNullable = false): GraphQLInputObjectType {
|
||||
const fieldToSchemaMap = {
|
||||
number: (field: Field) => ({ type: withNullableType(field, GraphQLFloat, forceNullable) }),
|
||||
number: (field: Field) => {
|
||||
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat;
|
||||
return { type: withNullableType(field, type, forceNullable) };
|
||||
},
|
||||
text: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
|
||||
email: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
|
||||
textarea: (field: Field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
|
||||
@@ -67,7 +84,7 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin
|
||||
relationship: (field: RelationshipField) => {
|
||||
const { relationTo } = field;
|
||||
type PayloadGraphQLRelationshipType = GraphQLScalarType | GraphQLList<GraphQLScalarType> | GraphQLInputObjectType;
|
||||
let type: PayloadGraphQLRelationshipType = GraphQLString;
|
||||
let type: PayloadGraphQLRelationshipType;
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
const fullName = `${combineParentName(parentName, field.label === false ? toWords(field.name, true) : field.label)}RelationshipInput`;
|
||||
@@ -85,9 +102,11 @@ function buildMutationInputType(name: string, fields: Field[], parentName: strin
|
||||
}), {}),
|
||||
}),
|
||||
},
|
||||
value: { type: GraphQLString },
|
||||
value: { type: GraphQLJSON },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
type = getCollectionIDType(payload.collections[relationTo].config);
|
||||
}
|
||||
|
||||
return { type: field.hasMany ? new GraphQLList(type) : type };
|
||||
|
||||
@@ -344,7 +344,7 @@ const buildWhereInputType = (name: string, fields: Field[], parentName: string):
|
||||
fieldTypes.id = {
|
||||
type: withOperators(
|
||||
{ name: 'id' } as Field,
|
||||
GraphQLString,
|
||||
GraphQLJSON,
|
||||
parentName,
|
||||
[...operators.equality, ...operators.contains],
|
||||
),
|
||||
|
||||
@@ -125,9 +125,6 @@ class ParamParser {
|
||||
let localizedKey = this.getLocalizedKey(sanitizedKey, schemaObject);
|
||||
if (key === '_id' || key === 'id') {
|
||||
localizedKey = '_id';
|
||||
if (!mongoose.Types.ObjectId.isValid(val)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (key.includes('.') || key.includes('__')) {
|
||||
const paths = key.split('.');
|
||||
|
||||
@@ -49,9 +49,18 @@ const formatBaseSchema = (field: Field) => ({
|
||||
|
||||
const buildSchema = (config: SanitizedConfig, configFields: Field[], options = {}): Schema => {
|
||||
let fields = {};
|
||||
let schemaFields = configFields;
|
||||
const indexFields = [];
|
||||
|
||||
configFields.forEach((field) => {
|
||||
const idField = schemaFields.find(({ name }) => name === 'id');
|
||||
if (idField) {
|
||||
fields = {
|
||||
_id: idField.type === 'number' ? Number : String,
|
||||
};
|
||||
schemaFields = schemaFields.filter(({ name }) => name !== 'id');
|
||||
}
|
||||
|
||||
schemaFields.forEach((field) => {
|
||||
const fieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type];
|
||||
|
||||
if (fieldSchema) {
|
||||
@@ -67,7 +76,9 @@ const buildSchema = (config: SanitizedConfig, configFields: Field[], options = {
|
||||
indexFields.forEach((index) => {
|
||||
schema.index(index);
|
||||
});
|
||||
|
||||
indexFields.forEach((index) => {
|
||||
schema.index(index);
|
||||
});
|
||||
setBlockDiscriminators(configFields, schema, config);
|
||||
|
||||
return schema;
|
||||
@@ -315,7 +326,7 @@ const fieldToSchemaMap = {
|
||||
upload: (field: UploadField, fields: SchemaDefinition, config: SanitizedConfig): SchemaDefinition => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field),
|
||||
type: Schema.Types.ObjectId,
|
||||
type: Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
};
|
||||
|
||||
@@ -350,14 +361,14 @@ const fieldToSchemaMap = {
|
||||
if (hasManyRelations) {
|
||||
localeSchema._id = false;
|
||||
localeSchema.value = {
|
||||
type: Schema.Types.ObjectId,
|
||||
type: Schema.Types.Mixed,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
};
|
||||
localeSchema.relationTo = { type: String, enum: field.relationTo };
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field),
|
||||
type: Schema.Types.ObjectId,
|
||||
type: Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
};
|
||||
}
|
||||
@@ -372,7 +383,7 @@ const fieldToSchemaMap = {
|
||||
} else if (hasManyRelations) {
|
||||
schemaToReturn._id = false;
|
||||
schemaToReturn.value = {
|
||||
type: Schema.Types.ObjectId,
|
||||
type: Schema.Types.Mixed,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
};
|
||||
schemaToReturn.relationTo = { type: String, enum: field.relationTo };
|
||||
@@ -381,7 +392,7 @@ const fieldToSchemaMap = {
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field),
|
||||
type: Schema.Types.ObjectId,
|
||||
type: Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user