Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ DATABASE_URL='mongodb://'
PAYLOAD_SECRET= # openssl rand -hex 32

NEXT_PUBLIC_SERVER_URL=http://localhost:3000
DRAFT_SECRET_TOKEN= # openssl rand -hex 32

SMTP_HOST=email-smtp.ap-southeast-1.amazonaws.com
SMTP_USER=
Expand Down
16 changes: 15 additions & 1 deletion src/access/anyone.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import type { Access } from 'payload'

export const anyone: Access = () => true
export const anyone: Access = ({ req: { user, headers } }) => {
if (user) {
return true
}
const authorization = headers.get('authorization')
if (
process.env.DRAFT_SECRET_TOKEN &&
authorization === `Bearer ${process.env.DRAFT_SECRET_TOKEN}`
) {
return true
}
return {
or: [{ _status: { equals: 'published' } }, { _status: { exists: false } }],
}
}
3 changes: 0 additions & 3 deletions src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1e
import { FolderTypeField as FolderTypeField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'

export const importMap = {
Expand Down Expand Up @@ -47,7 +46,5 @@ export const importMap = {
'@payloadcms/next/client#FolderTypeField': FolderTypeField_2b8867833a34864a02ddf429b0728a40,
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
'@payloadcms/storage-s3/client#S3ClientUploadHandler':
S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
'@payloadcms/next/rsc#CollectionCards': CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { AccessType } from '@/constants/accessTypes'
import { COLLECTION_LABELS, getCollectionGroupLabel } from '@/constants/collections'
import type { AccessArgs, CollectionConfig } from 'payload'

const checkSampleAccess = (accessType?: AccessType) => (access: AccessArgs) =>
checkCollectionAccess(access, 'sample', accessType)
const checkSampleAccess: (
accessType?: AccessType,
) => (access: AccessArgs) => ReturnType<typeof checkCollectionAccess> =
(accessType?: AccessType) => (access: AccessArgs) =>
checkCollectionAccess(access, 'sample', accessType)

export const Sample: CollectionConfig = {
slug: 'sample',
Expand Down
2 changes: 1 addition & 1 deletion src/constants/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function getCollectionGroupLabel(groupSlug: CollectionGroupSlug) {
}

export const COLLECTION_GROUP_ITEMS: Record<CollectionGroupSlug, CollectionSlug[]> = {
'durianpy-website': [],
'durianpy-website': ['sample', 'users'],
admin: ['users'],
} as const

Expand Down
152 changes: 152 additions & 0 deletions src/endpoints/durianpy-website-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import fs from 'fs'
import path from 'path'

import type { Endpoint } from 'payload'

import { getCollectionGroupItems } from '@/constants/collections'

const GROUP_SLUG = 'durianpy-website' as const

function getCommentStartBefore(content: string, index: number): number {
const commentEnd = content.lastIndexOf('*/', index)
if (commentEnd === -1) return index

const commentStart = content.lastIndexOf('/**', commentEnd)
if (commentStart === -1) return index

const between = content.slice(commentEnd + 2, index)
return /^\s*$/.test(between) ? commentStart : index
}

function extractInterfaceBlock(content: string, interfaceName: string): string | null {
const declaration = `export interface ${interfaceName}`
const declarationIndex = content.indexOf(declaration)

if (declarationIndex === -1) return null

const startIndex = getCommentStartBefore(content, declarationIndex)
const braceStart = content.indexOf('{', declarationIndex)

if (braceStart === -1) return null

let braceDepth = 0
for (let i = braceStart; i < content.length; i += 1) {
const char = content[i]

if (char === '{') braceDepth += 1
if (char === '}') braceDepth -= 1

if (braceDepth === 0) {
return content.slice(startIndex, i + 1)
}
}

return null
}

function getCollectionTypeMapFromConfig(typesFileContent: string): Map<string, string> {
const map = new Map<string, string>()
const collectionsBlockMatch = typesFileContent.match(/collections:\s*\{([\s\S]*?)\n\s*\};/)

if (!collectionsBlockMatch) return map

const collectionsBlock = collectionsBlockMatch[1]
const collectionEntryRegex = /^\s*(?:'([^']+)'|([A-Za-z0-9_-]+)):\s*([A-Za-z0-9_]+);\s*$/gm

let entryMatch: RegExpExecArray | null
while ((entryMatch = collectionEntryRegex.exec(collectionsBlock)) !== null) {
const slug = entryMatch[1] ?? entryMatch[2]
const typeName = entryMatch[3]

if (!slug || !typeName) continue
map.set(slug, typeName)
}

return map
}

export const durianpyWebsiteTypesEndpoint: Endpoint = {
method: 'get',
path: '/durianpy-website-types',
handler: async () => {
try {
const fullTypes = fs.readFileSync(
path.resolve(process.cwd(), 'src/payload-types.ts'),
'utf-8',
)

const groupCollectionSlugs = [...getCollectionGroupItems(GROUP_SLUG)].sort((a, b) =>
a.localeCompare(b),
)

const collectionTypeMap = getCollectionTypeMapFromConfig(fullTypes)

const missingTypeMappings: string[] = []
const selectedTypeNames = groupCollectionSlugs
.map((slug) => {
const typeName = collectionTypeMap.get(slug)
if (!typeName) missingTypeMappings.push(slug)
return typeName
})
.filter((name): name is string => Boolean(name))

if (missingTypeMappings.length > 0) {
return new Response(
JSON.stringify({
error: 'Missing collection to type mappings in payload-types.ts',
group: GROUP_SLUG,
missingCollectionSlugs: missingTypeMappings,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}

const missingInterfaceBlocks: string[] = []
const extractedInterfaces = selectedTypeNames
.map((typeName) => {
const block = extractInterfaceBlock(fullTypes, typeName)
if (!block) missingInterfaceBlocks.push(typeName)
return block
})
.filter((block): block is string => Boolean(block))

if (missingInterfaceBlocks.length > 0) {
return new Response(
JSON.stringify({
error: 'Could not extract one or more interfaces from payload-types.ts',
group: GROUP_SLUG,
missingInterfaces: missingInterfaceBlocks,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}

const typeSyncHeader = `// Auto-generated from src/payload-types.ts\n// Group: ${GROUP_SLUG}\n// Collections: ${groupCollectionSlugs.join(', ')}`
const payload = `${typeSyncHeader}\n\n${extractedInterfaces.join('\n\n')}`

return new Response(payload, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-store',
},
})
} catch (error) {
return new Response(
JSON.stringify({
error: 'Failed to generate durianpy-website type sync payload',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
},
}
2 changes: 2 additions & 0 deletions src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { plugins } from './plugins'
import { defaultLexical } from '@/fields/defaultLexical'
import { getServerSideURL, getServerSideOrigin } from './utilities/getURL'
import { Sample } from './collections/durianpy-website/sample-website-collection.index'
import { durianpyWebsiteTypesEndpoint } from './endpoints/durianpy-website-types'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
Expand Down Expand Up @@ -94,6 +95,7 @@ export default buildConfig({
}),
}),
collections: [Media, Categories, Users, Sample],
endpoints: [durianpyWebsiteTypesEndpoint],
cors: [getServerSideOrigin()].filter(Boolean).map((url) => {
try {
const { origin } = new URL(url!)
Expand Down
19 changes: 19 additions & 0 deletions tests/int/api.int.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPayload, Payload } from 'payload'
import config from '@/payload.config'
import { GET as getDurianTypes } from '@/app/(payload)/api/durianpy-website-types/route'

import { describe, it, beforeAll, expect } from 'vitest'

Expand All @@ -17,4 +18,22 @@ describe('API', () => {
})
expect(users).toBeDefined()
})

it('returns only durianpy-website collection interfaces', async () => {
const response = await getDurianTypes()

expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toContain('text/plain')

const body = await response.text()

expect(body).toContain('Group: durianpy-website')
expect(body).toContain('Collections: sample, users')
expect(body).toContain('export interface Sample')
expect(body).toContain('export interface User')

// Ensure non-group collections are excluded.
expect(body).not.toContain('export interface Media')
expect(body).not.toContain('export interface Category')
})
})
Loading