feat(db-postgres): add point field support (#9078)

### What?
Adds full support for the point field to Postgres and Vercel Postgres
adapters through the Postgis extension. Fully the same API as with
MongoDB, including support for `near`, `within` and `intersects`
operators.

Additionally, exposes to adapter args:
*
`tablesFilter`https://orm.drizzle.team/docs/drizzle-kit-push#including-tables-schemas-and-extensions.
* `extensions` list of extensions to create, for example `['vector',
'pg_search']`, `postgis` is created automatically if there's any point
field

### Why?
It's essential to support that field type, especially if the postgres
adapter should be out of beta on 3.0 stable.

### How?
* Bumps `drizzle-orm` to `0.36.1` and `drizzle-kit` to `0.28.0` as we
need this change https://github.com/drizzle-team/drizzle-orm/pull/3141
* Uses its functions to achieve querying functionality, for example the
`near` operator works through `ST_DWithin` or `intersects` through
`ST_Intersects`.
* Removes MongoDB condition from all point field tests, but keeps for
SQLite

Resolves these discussions:
https://github.com/payloadcms/payload/discussions/8996
https://github.com/payloadcms/payload/discussions/8644
This commit is contained in:
Sasha
2024-11-11 16:31:47 +02:00
committed by GitHub
parent a421503111
commit 0a15388edb
44 changed files with 887 additions and 404 deletions

View File

@@ -19,6 +19,16 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
posts: PostsSelect<false> | PostsSelect<true>;
simple: SimpleSelect<false> | SimpleSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
@@ -26,6 +36,10 @@ export interface Config {
menu: Menu;
'custom-ts': CustomT;
};
globalsSelect: {
menu: MenuSelect<false> | MenuSelect<true>;
'custom-ts': CustomTsSelect<false> | CustomTsSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
@@ -232,6 +246,143 @@ export interface PayloadMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
text?: T;
serverTextField?: T;
richText?: T;
myBlocks?:
| T
| {
test?:
| T
| {
test?: T;
id?: T;
blockName?: T;
};
someBlock2?:
| T
| {
test2?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "simple_select".
*/
export interface SimpleSelect<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
sizes?:
| T
| {
thumbnail?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
medium?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
large?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "menu".
@@ -265,6 +416,28 @@ export interface CustomT {
export interface ObjectWithNumber {
id?: number;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "menu_select".
*/
export interface MenuSelect<T extends boolean = true> {
globalText?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-ts_select".
*/
export interface CustomTsSelect<T extends boolean = true> {
custom?: T;
withDefinitionsUsage?: T;
json?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".

View File

@@ -29,7 +29,7 @@ describe('collections-graphql', () => {
if (payload.db.name === 'mongoose') {
await new Promise((resolve, reject) => {
payload.db?.collections?.point?.ensureIndexes(function (err) {
if (err) reject(err)
if (err) {reject(err)}
resolve(true)
})
})
@@ -578,13 +578,13 @@ describe('collections-graphql', () => {
expect(docs).toContainEqual(expect.objectContaining({ id: specialPost.id }))
})
if (['mongodb'].includes(process.env.PAYLOAD_DATABASE)) {
describe('near', () => {
const point = [10, 20]
const [lat, lng] = point
describe('near', () => {
const point = [10, 20]
const [lat, lng] = point
it('should return a document near a point', async () => {
const nearQuery = `
it('should return a document near a point', async () => {
if (payload.db.name === 'sqlite') {return}
const nearQuery = `
query {
Points(
where: {
@@ -600,16 +600,17 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(1)
})
expect(docs).toHaveLength(1)
})
it('should not return a point far away', async () => {
const nearQuery = `
it('should not return a point far away', async () => {
if (payload.db.name === 'sqlite') {return}
const nearQuery = `
query {
Points(
where: {
@@ -625,29 +626,30 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(0)
expect(docs).toHaveLength(0)
})
it('should sort find results by nearest distance', async () => {
if (payload.db.name === 'sqlite') {return}
// creating twice as many records as we are querying to get a random sample
await mapAsync([...Array(10)], async () => {
// randomize the creation timestamp
await wait(Math.random())
await payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
})
})
it('should sort find results by nearest distance', async () => {
// creating twice as many records as we are querying to get a random sample
await mapAsync([...Array(10)], async () => {
// randomize the creation timestamp
await wait(Math.random())
await payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
})
})
const nearQuery = `
const nearQuery = `
query {
Points(
where: {
@@ -655,6 +657,7 @@ describe('collections-graphql', () => {
near: [0, 0, 100000, 0]
}
},
sort: "point",
limit: 5
) {
docs {
@@ -664,32 +667,33 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query: nearQuery }) })
.then((res) => res.json())
const { docs } = data.Points
let previous = 0
docs.forEach(({ point: coordinates }) => {
// The next document point should always be greater than the one before
expect(previous).toBeLessThanOrEqual(coordinates[0])
;[previous] = coordinates
})
let previous = 0
docs.forEach(({ point: coordinates }) => {
// The next document point should always be greater than the one before
expect(previous).toBeLessThanOrEqual(coordinates[0])
;[previous] = coordinates
})
})
})
describe('within', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
describe('within', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point inside the polygon', async () => {
const query = `
it('should return a document with the point inside the polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const query = `
query {
Points(
where: {
@@ -707,18 +711,19 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(1)
expect(docs[0].point).toEqual([10, 20])
})
expect(docs).toHaveLength(1)
expect(docs[0].point).toEqual([10, 20])
})
it('should not return a document with the point outside the polygon', async () => {
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1))
const query = `
it('should not return a document with the point outside the polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1))
const query = `
query {
Points(
where: {
@@ -736,27 +741,28 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(0)
})
expect(docs).toHaveLength(0)
})
})
describe('intersects', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
describe('intersects', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point intersecting the polygon', async () => {
const query = `
it('should return a document with the point intersecting the polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const query = `
query {
Points(
where: {
@@ -774,18 +780,19 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(1)
expect(docs[0].point).toEqual([10, 20])
})
expect(docs).toHaveLength(1)
expect(docs[0].point).toEqual([10, 20])
})
it('should not return a document with the point not intersecting a smaller polygon', async () => {
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1))
const query = `
it('should not return a document with the point not intersecting a smaller polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1))
const query = `
query {
Points(
where: {
@@ -803,15 +810,14 @@ describe('collections-graphql', () => {
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const { docs } = data.Points
expect(docs).toHaveLength(0)
})
expect(docs).toHaveLength(0)
})
}
})
it('can query deeply nested fields within rows, tabs, collapsibles', async () => {
const withNestedField = await createPost({ D1: { D2: { D3: { D4: 'nested message' } } } })

View File

@@ -1103,185 +1103,194 @@ describe('collections-rest', () => {
})
})
if (['mongodb'].includes(process.env.PAYLOAD_DATABASE)) {
describe('near', () => {
const point = [10, 20]
const [lat, lng] = point
it('should return a document near a point', async () => {
const near = `${lat + 0.01}, ${lng + 0.01}, 10000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
describe('near', () => {
const point = [10, 20]
const [lat, lng] = point
it('should return a document near a point', async () => {
if (payload.db.name === 'sqlite') {return}
const near = `${lat + 0.01}, ${lng + 0.01}, 10000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
},
})
const result = await response.json()
it('should not return a point far away', async () => {
const near = `${lng + 1}, ${lat - 1}, 5000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
it('should sort find results by nearest distance', async () => {
// creating twice as many records as we are querying to get a random sample
const promises = []
for (let i = 0; i < 11; i++) {
// setTimeout used to randomize the creation timestamp
setTimeout(() => {
promises.push(
payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
}),
)
}, Math.random())
}
await Promise.all(promises)
const { docs } = await restClient
.GET(`/${pointSlug}`, {
query: {
limit: 5,
where: {
point: {
// querying large enough range to include all docs
near: '0, 0, 100000, 0',
},
},
},
})
.then((res) => res.json())
let previous = 0
docs.forEach(({ point: coordinates }) => {
// the next document point should always be greater than the one before
expect(previous).toBeLessThanOrEqual(coordinates[0])
;[previous] = coordinates
})
})
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
describe('within', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point inside the polygon', async () => {
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
it('should not return a point far away', async () => {
if (payload.db.name === 'sqlite') {return}
const near = `${lng + 1}, ${lat + 1}, 5000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
},
})
const result = await response.json()
it('should not return a document with the point outside a smaller polygon', async () => {
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
describe('intersects', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should sort find results by nearest distance', async () => {
if (payload.db.name === 'sqlite') {return}
it('should return a document with the point intersecting the polygon', async () => {
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
// creating twice as many records as we are querying to get a random sample
const promises = []
for (let i = 0; i < 11; i++) {
// setTimeout used to randomize the creation timestamp
setTimeout(() => {
promises.push(
payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
}),
)
}, Math.random())
}
await Promise.all(promises)
const { docs } = await restClient
.GET(`/${pointSlug}`, {
query: {
limit: 5,
sort: 'point',
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon],
},
// querying large enough range to include all docs
near: '0, 0, 100000, 0',
},
},
},
})
const result = await response.json()
.then((res) => res.json())
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not return a document with the point not intersecting a smaller polygon', async () => {
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
let previous = 0
docs.forEach(({ point: coordinates }) => {
// the next document point should always be greater than the one before
expect(previous).toBeLessThanOrEqual(coordinates[0])
;[previous] = coordinates
})
})
}
})
describe('within', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point inside the polygon', async () => {
if (payload.db.name === 'sqlite') {return}
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not return a document with the point outside a smaller polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
})
describe('intersects', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point intersecting the polygon', async () => {
if (payload.db.name === 'sqlite') {return}
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not return a document with the point not intersecting a smaller polygon', async () => {
if (payload.db.name === 'sqlite') {return}
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
})
it('or', async () => {
const post1 = await createPost({ title: 'post1' })

View File

@@ -103,6 +103,11 @@ export default buildConfigWithDefaults({
{ value: 'default', label: 'Default' },
],
},
{
name: 'point',
type: 'point',
defaultValue: [10, 20],
},
],
},
{

View File

@@ -598,6 +598,10 @@ describe('database', () => {
expect(result.array[0].defaultValue).toStrictEqual('default value from database')
expect(result.group.defaultValue).toStrictEqual('default value from database')
expect(result.select).toStrictEqual('default')
// eslint-disable-next-line jest/no-conditional-in-test
if (payload.db.name !== 'sqlite') {
expect(result.point).toStrictEqual({ coordinates: [10, 20], type: 'Point' })
}
})
})
describe('drizzle: schema hooks', () => {

View File

@@ -25,6 +25,7 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
posts: PostsSelect<false> | PostsSelect<true>;
'default-values': DefaultValuesSelect<false> | DefaultValuesSelect<true>;
@@ -106,6 +107,11 @@ export interface DefaultValue {
defaultValue?: string | null;
};
select?: ('option0' | 'option1' | 'default') | null;
/**
* @minItems 2
* @maxItems 2
*/
point?: [number, number] | null;
updatedAt: string;
createdAt: string;
}
@@ -263,6 +269,7 @@ export interface CustomId {
id: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -400,6 +407,7 @@ export interface DefaultValuesSelect<T extends boolean = true> {
defaultValue?: T;
};
select?: T;
point?: T;
updatedAt?: T;
createdAt?: T;
}
@@ -531,6 +539,7 @@ export interface CustomIdsSelect<T extends boolean = true> {
id?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -982,88 +982,91 @@ describe('Fields', () => {
expect(definitions['version.text']).toEqual(1)
})
})
describe('point', () => {
let doc
const point = [7, -7]
const localized = [5, -2]
const group = { point: [1, 9] }
beforeEach(async () => {
const findDoc = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = findDoc.docs
})
it('should read', async () => {
const find = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = find.docs
expect(doc.point).toEqual(pointDoc.point)
expect(doc.localized).toEqual(pointDoc.localized)
expect(doc.group).toMatchObject(pointDoc.group)
})
it('should create', async () => {
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
it('should not create duplicate point when unique', async () => {
// first create the point field
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
// Now make sure we can't create a duplicate (since 'localized' is a unique field)
await expect(() =>
payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
}),
).rejects.toThrow(Error)
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
min: 5,
},
}),
).rejects.toThrow('The following field is invalid: min')
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
})
}
describe('point', () => {
let doc
const point = [7, -7]
const localized = [5, -2]
const group = { point: [1, 9] }
beforeEach(async () => {
const findDoc = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = findDoc.docs
})
it('should read', async () => {
if (payload.db.name === 'sqlite') {return}
const find = await payload.find({
collection: 'point-fields',
pagination: false,
})
;[doc] = find.docs
expect(doc.point).toEqual(pointDoc.point)
expect(doc.localized).toEqual(pointDoc.localized)
expect(doc.group).toMatchObject(pointDoc.group)
})
it('should create', async () => {
if (payload.db.name === 'sqlite') {return}
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
it('should not create duplicate point when unique', async () => {
if (payload.db.name === 'sqlite') {return}
// first create the point field
doc = await payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
})
// Now make sure we can't create a duplicate (since 'localized' is a unique field)
await expect(() =>
payload.create({
collection: 'point-fields',
data: {
group,
localized,
point,
},
}),
).rejects.toThrow(Error)
await expect(async () =>
payload.create({
collection: 'number-fields',
data: {
min: 5,
},
}),
).rejects.toThrow('The following field is invalid: min')
expect(doc.point).toEqual(point)
expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group)
})
})
describe('unique indexes', () => {
it('should throw validation error saving on unique fields', async () => {
const data = {

View File

@@ -64,7 +64,7 @@
"comment-json": "^4.2.3",
"create-payload-app": "workspace:*",
"dotenv": "16.4.5",
"drizzle-kit": "0.26.2",
"drizzle-kit": "0.28.0",
"eslint-plugin-playwright": "1.7.0",
"execa": "5.1.1",
"file-type": "19.3.0",

View File

@@ -0,0 +1,15 @@
import type { CollectionConfig } from 'payload'
export const Points: CollectionConfig = {
slug: 'points',
fields: [
{
type: 'text',
name: 'text',
},
{
type: 'point',
name: 'point',
},
],
}

View File

@@ -7,6 +7,7 @@ import { devUser } from '../credentials.js'
import { DeepPostsCollection } from './collections/DeepPosts/index.js'
import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js'
import { Pages } from './collections/Pages/index.js'
import { Points } from './collections/Points/index.js'
import { PostsCollection } from './collections/Posts/index.js'
import { VersionedPostsCollection } from './collections/VersionedPosts/index.js'
@@ -21,6 +22,7 @@ export default buildConfigWithDefaults({
VersionedPostsCollection,
DeepPostsCollection,
Pages,
Points,
],
globals: [
{

View File

@@ -9,6 +9,7 @@ import type {
GlobalPost,
LocalizedPost,
Page,
Point,
Post,
VersionedPost,
} from './payload-types.js'
@@ -40,14 +41,21 @@ describe('Select', () => {
let post: Post
let postId: number | string
let point: Point
let pointId: number | string
beforeEach(async () => {
post = await createPost()
postId = post.id
point = await createPoint()
pointId = point.id
})
// Clean up to safely mutate in each test
afterEach(async () => {
await payload.delete({ id: postId, collection: 'posts' })
await payload.delete({ id: pointId, collection: 'points' })
})
describe('Include mode', () => {
@@ -307,6 +315,23 @@ describe('Select', () => {
),
})
})
it('should select a point field', async () => {
if (payload.db.name === 'sqlite') {
return
}
const res = await payload.findByID({
collection: 'points',
id: pointId,
select: { point: true },
})
expect(res).toStrictEqual({
id: pointId,
point: point.point,
})
})
})
describe('Exclude mode', () => {
@@ -486,6 +511,23 @@ describe('Select', () => {
}),
})
})
it('should exclude a point field', async () => {
if (payload.db.name === 'sqlite') {
return
}
const res = await payload.findByID({
collection: 'points',
id: pointId,
select: { point: false },
})
const copy = { ...point }
delete copy['point']
expect(res).toStrictEqual(copy)
})
})
})
@@ -2032,3 +2074,7 @@ function createVersionedPost() {
},
})
}
function createPoint() {
return payload.create({ collection: 'points', data: { text: 'some', point: [10, 20] } })
}

View File

@@ -16,6 +16,7 @@ export interface Config {
'versioned-posts': VersionedPost;
'deep-posts': DeepPost;
pages: Page;
points: Point;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -28,6 +29,7 @@ export interface Config {
'versioned-posts': VersionedPostsSelect<false> | VersionedPostsSelect<true>;
'deep-posts': DeepPostsSelect<false> | DeepPostsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
points: PointsSelect<false> | PointsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -306,6 +308,21 @@ export interface Page {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "points".
*/
export interface Point {
id: string;
text?: string | null;
/**
* @minItems 2
* @maxItems 2
*/
point?: [number, number] | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -350,6 +367,10 @@ export interface PayloadLockedDocument {
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'points';
value: string | Point;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -631,6 +652,16 @@ export interface PagesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "points_select".
*/
export interface PointsSelect<T extends boolean = true> {
text?: T;
point?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".