Skip to content

Commit

Permalink
update hooks for 0.23 support (fixes #2)
Browse files Browse the repository at this point in the history
  • Loading branch information
satohshi committed Dec 4, 2024
1 parent 8ab2a16 commit 11454c6
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 193 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-fireants-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pocketbase-ts-schema-generator': minor
---

update hooks for 0.23 support (fixes #2)
170 changes: 40 additions & 130 deletions src/generate-docs.ts
Original file line number Diff line number Diff line change
@@ -1,138 +1,53 @@
interface BaseSchemaFields {
system: boolean
id: string
name: string
required: boolean
presentable: boolean
unique: boolean
}

interface TextSchemaFields extends BaseSchemaFields {
type: 'text'
options: {
min: number | null
max: number | null
pattern: string
}
}

interface EditorSchemaFields extends BaseSchemaFields {
type: 'editor'
options: {
convertUrls: boolean
}
}

interface NumberSchemaFields extends BaseSchemaFields {
type: 'number'
options: {
min: number | null
max: number | null
noDecimal: boolean
}
}

interface BoolSchemaFields extends BaseSchemaFields {
type: 'bool'
options: {}
}

interface EmailSchemaFields extends BaseSchemaFields {
type: 'email'
options: {
exceptDomains: string[] | null
onlyDomains: string[] | null
}
}

interface UrlSchemaFields extends BaseSchemaFields {
type: 'url'
options: {
exceptDomains: string[] | null
onlyDomains: string[] | null
}
}

interface DateSchemaFields extends BaseSchemaFields {
type: 'date'
options: {
min: string
max: string
}
}

interface SelectSchemaFields extends BaseSchemaFields {
type: 'select'
options: {
maxSelect: number
values: string[]
isMultiple: () => boolean
}
}

interface FileSchemaFields extends BaseSchemaFields {
type: 'file'
options: {
mimeTypes: string[]
thumbs: string[]
maxSelect: number
maxSize: number
protected: boolean
isMultiple: () => boolean
}
}

interface RelationSchemaFields extends BaseSchemaFields {
type: 'relation'
options: {
collectionId: string
cascadeDelete: boolean
minSelect: number | null
maxSelect: number | null
displayFields: string[] | null
isMultiple: () => boolean
}
}

interface JsonSchemaFields extends BaseSchemaFields {
type: 'json'
options: {
maxSize: number
}
}

export type SchemaFields =
| TextSchemaFields
| EditorSchemaFields
| NumberSchemaFields
| BoolSchemaFields
| EmailSchemaFields
| UrlSchemaFields
| DateSchemaFields
| SelectSchemaFields
| FileSchemaFields
| RelationSchemaFields
| JsonSchemaFields
export type SchemaField =
| TextField
| EditorField
| NumberField
| BoolField
| EmailField
| URLField
| DateField
| SelectField
| FileField
| RelationField
| JSONField

export const generateDocString = (
options: SchemaFields,
field: SchemaField,
multiple: boolean,
collectionMap: Record<string, string>
): string => {
const type = field.type()

// remove options that are not needed in the table
const optionEntries = Object.entries(options.options)
const optionEntries = Object.entries(field)
.filter(([key, value]) => {
const unnecessaryKeys = ['isMultiple', 'validate', 'displayFields', 'values']
const unnecessaryKeys = [
'id',
'name',
'system',
'presentable',
'values',
'primaryKey',
'cost',
]
if (!multiple) {
unnecessaryKeys.push('maxSelect', 'minSelect')
}

const isFunction = typeof value === 'function'

// value of `min` and `max` is of type `object`, not array, string, or number
const hasValue =
value !== null &&
(Array.isArray(value) ? value.length > 0 : value.toString() !== '')

return hasValue && !unnecessaryKeys.includes(key)
// `0` means not defined in these cases
const notDefined =
type !== 'number' &&
['max', 'min', 'maxSize', 'minSelect', 'maxSelect'].includes(key) &&
value === 0

return !isFunction && hasValue && !notDefined && !unnecessaryKeys.includes(key)
})
// wrap values in backticks
.map<[string, string]>(([key, value]) => {
Expand All @@ -142,24 +57,20 @@ export const generateDocString = (
return [key, `\`${value}\``]
})

const typeNameStr = `\`${options.type}${
['file', 'relation', 'select'].includes(options.type)
? multiple
? '(multiple)'
: '(single)'
: ''
const typeNameStr = `\`${type}${
['file', 'relation', 'select'].includes(type) ? (multiple ? '(multiple)' : '(single)') : ''
}\``
const requiredStr = `\`${options.required}\``
const requiredStr = `\`${field.required}\``
const relatedCollectionNameStr =
options.type === 'relation' ? `\`${collectionMap[options.options.collectionId]}\`` : ''
type === 'relation' ? `\`${collectionMap[(field as RelationField).collectionId]}\`` : ''

// column width
const leftColWidth = Math.max(
options.type === 'relation' ? 14 : 8, // length of `collectionName` is 14
type === 'relation' ? 14 : 8, // length of `collectionName` is 14
...optionEntries.map(([key]) => key.length)
)
const rightColWidth = Math.max(
options.type === 'relation' ? 15 : 0, // id length
type === 'relation' ? 15 : 0, // id length
relatedCollectionNameStr.length, // collection name length
typeNameStr.length,
requiredStr.length,
Expand All @@ -171,7 +82,6 @@ export const generateDocString = (
`| ${' '.repeat(leftColWidth)} | ${' '.repeat(rightColWidth)} |`,
`| ${'-'.repeat(leftColWidth)} | ${'-'.repeat(rightColWidth)} |`,
`| ${'type'.padEnd(leftColWidth, ' ')} | ${typeNameStr.padEnd(rightColWidth, ' ')} |`,
`| ${'required'.padEnd(leftColWidth, ' ')} | ${requiredStr.padEnd(rightColWidth, ' ')} |`,
]

for (const [key, value] of optionEntries) {
Expand Down
81 changes: 22 additions & 59 deletions src/generate-schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SchemaFields, generateDocString } from './generate-docs'
import { SchemaField, generateDocString } from './generate-docs'
import { haveSameValues, newLine, toPascalCase } from './utils'
import config from './config.json'

Expand All @@ -14,29 +14,6 @@ const UNIQUE_IDENTIFIER = `
readonly [uniqueIdentifier]: unique symbol
`

const BASE_COLLECTION_INTERFACE = `interface BaseCollection {
id: string
created: string
updated: string
}`

const AUTH_COLLECTION_INTERFACE = `interface AuthCollection extends BaseCollection {
username: string
email: string
emailVisibility: boolean
verified: boolean
}`

const VIEW_COLLECTION_INTERFACE = `interface ViewCollection {
id: string
}`

const COLLECTOIN_TYPE_MAP: Record<string, string> = {
base: 'BaseCollection',
auth: 'AuthCollection',
view: 'ViewCollection',
}

const TYPE_MAP: Record<string, string> = {
number: 'number',
bool: 'boolean',
Expand All @@ -45,48 +22,38 @@ const TYPE_MAP: Record<string, string> = {
}

export default () => {
const allCollections = [
...$app.dao().findCollectionsByType('auth'),
...$app.dao().findCollectionsByType('base'),
...$app.dao().findCollectionsByType('view'),
] as Array<models.Collection>
const allCollections = $app.findAllCollections() as Array<core.Collection>

const collectionIdToNameMap = Object.fromEntries(
allCollections.map((collection) => [collection.id, collection.name])
)

// interfaces
let header = newLine(0, BASE_COLLECTION_INTERFACE, 2) + newLine(0, AUTH_COLLECTION_INTERFACE, 2)
let collectionInterfaces = ''
const fieldSets: Set<string>[] = []
for (const collection of allCollections) {
const fields = new Set<string>()

// only add view interface if needed
if (collection.type === 'view' && !header.includes(VIEW_COLLECTION_INTERFACE)) {
header += newLine(0, VIEW_COLLECTION_INTERFACE, 2)
}
collectionInterfaces += newLine(0, `export interface ${toPascalCase(collection.name)} {`)

collectionInterfaces += newLine(
0,
`export interface ${toPascalCase(collection.name)} extends ${
COLLECTOIN_TYPE_MAP[collection.type]
} {`
)

for (const field of collection.schema.fields() as Array<SchemaField>) {
const { type, name, options } = field
const multipleValues = options.isMultiple?.() ?? false
for (const fieldOptions of collection.fields as Array<core.Field>) {
const name = fieldOptions.name
const type = fieldOptions.type()
const multipleValues = fieldOptions.isMultiple?.() ?? false

if (config.includeDocstring) {
collectionInterfaces += newLine(
1,
generateDocString(field as SchemaFields, multipleValues, collectionIdToNameMap)
generateDocString(
fieldOptions as SchemaField,
multipleValues,
collectionIdToNameMap
)
)
}

if (type === 'select') {
const selectOptions = options.values.map((v: string) => `'${v}'`).join(' | ')
const selectOptions = fieldOptions.values.map((v: string) => `'${v}'`).join(' | ')

const field = `${name}: ${multipleValues ? `(${selectOptions})[]` : selectOptions}`
fields.add(field)
Expand All @@ -105,8 +72,8 @@ export default () => {
// add unique identifier if there are collections with the same set of fields
if (fieldSets.some((set) => haveSameValues(set, fields))) {
// add unique identifier at the top if there are collections with the same set of fields
if (!header.includes(UNIQUE_IDENTIFIER_KEY)) {
header = newLine(0, UNIQUE_IDENTIFIER_KEY, 2) + header
if (!collectionInterfaces.includes(UNIQUE_IDENTIFIER_KEY)) {
collectionInterfaces = newLine(0, UNIQUE_IDENTIFIER_KEY, 2) + collectionInterfaces
}
collectionInterfaces += newLine(1, UNIQUE_IDENTIFIER)
}
Expand All @@ -115,28 +82,24 @@ export default () => {

fieldSets.push(fields)
}
collectionInterfaces = header + collectionInterfaces

// relations
const collectionToRelationMap: Record<string, string[]> = {}
for (const collection of allCollections) {
const fieldsWithUniqueIndex = new Set(
collection.indexes
.filter((index) => {
return index.includes('UNIQUE') && !index.includes('\n')
})
.map((index) => /^CREATE UNIQUE.+\(`(.+)`\)$/.exec(index)![1])
.filter((index) => index.includes('UNIQUE') && !index.includes(','))
.map((index) => /^CREATE UNIQUE.+\(`?([^`\s]+).*\).*/.exec(index)![1])
)

for (const fieldSchema of collection.schema.fields() as Array<SchemaField>) {
for (const fieldSchema of collection.fields as Array<core.Field>) {
// has to be outside if block for collections without relations
collectionToRelationMap[collection.name] ??= []

if (fieldSchema.type === 'relation') {
if (fieldSchema.type() === 'relation') {
const isOptional = !fieldSchema.required
const isToMany = Number(fieldSchema.options.maxSelect) !== 1
const relatedCollectionName =
collectionIdToNameMap[fieldSchema.options.collectionId]!
const isToMany = fieldSchema.isMultiple()

const relatedCollectionName = collectionIdToNameMap[fieldSchema.collectionId]!
const hasUniqueConstraint = fieldsWithUniqueIndex.has(fieldSchema.name)

// Forward relation
Expand Down
12 changes: 8 additions & 4 deletions src/main.pb.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
onAfterBootstrap(() => {
onBootstrap((e) => {
e.next()
require(`${__hooks}/generate-schema.js`).default()
})

onCollectionAfterCreateRequest(() => {
onCollectionCreateRequest((e) => {
e.next()
require(`${__hooks}/generate-schema.js`).default()
})

onCollectionAfterUpdateRequest(() => {
onCollectionUpdateRequest((e) => {
e.next()
require(`${__hooks}/generate-schema.js`).default()
})

onCollectionAfterDeleteRequest(() => {
onCollectionDeleteRequest((e) => {
e.next()
require(`${__hooks}/generate-schema.js`).default()
})

0 comments on commit 11454c6

Please sign in to comment.