Refactor fields (#48)

* separate out field handling into new file

* sort fields

* formatting
This commit is contained in:
Patrick Moody
2023-02-17 20:15:17 -08:00
committed by GitHub
parent 4fa58b153b
commit b3f95a891b
8 changed files with 391 additions and 384 deletions

View File

@@ -18,6 +18,10 @@
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"sort-keys-fix/sort-keys-fix": "warn"
"sort-keys-fix/sort-keys-fix": [
"warn",
"asc",
{ "allowLineSeparatedGroups": true }
]
}
}

96
src/fields.ts Normal file
View File

@@ -0,0 +1,96 @@
import {
DATE_STRING_TYPE_NAME,
HTML_STRING_NAME,
RECORD_ID_STRING_NAME,
} from "./constants"
import { getOptionEnumName, getOptionValues, sanitizeFieldName } from "./utils"
import { FieldSchema } from "./types"
import { fieldNameToGeneric } from "./generics"
/**
* Convert the pocketbase field type to the equivalent typescript type
*/
export const pbSchemaTypescriptMap = {
// Basic fields
bool: "boolean",
date: DATE_STRING_TYPE_NAME,
editor: HTML_STRING_NAME,
email: "string",
text: "string",
url: "string",
number: "number",
// Dependent on schema
file: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? "string[]"
: "string",
json: (fieldSchema: FieldSchema) =>
`null | ${fieldNameToGeneric(fieldSchema.name)}`,
relation: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect === 1
? RECORD_ID_STRING_NAME
: `${RECORD_ID_STRING_NAME}[]`,
select: (fieldSchema: FieldSchema, collectionName: string) => {
// pocketbase v0.8+ values are required
const valueType = fieldSchema.options.values
? getOptionEnumName(collectionName, fieldSchema.name)
: "string"
return fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${valueType}[]`
: valueType
},
// DEPRECATED: PocketBase v0.8 does not have a dedicated user relation
user: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${RECORD_ID_STRING_NAME}[]`
: RECORD_ID_STRING_NAME,
}
export function createTypeField(
collectionName: string,
fieldSchema: FieldSchema
): string {
let typeStringOrFunc:
| string
| ((fieldSchema: FieldSchema, collectionName: string) => string)
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
console.log(`WARNING: unknown type "${fieldSchema.type}" found in schema`)
typeStringOrFunc = "unknown"
} else {
typeStringOrFunc =
pbSchemaTypescriptMap[
fieldSchema.type as keyof typeof pbSchemaTypescriptMap
]
}
const typeString =
typeof typeStringOrFunc === "function"
? typeStringOrFunc(fieldSchema, collectionName)
: typeStringOrFunc
const fieldName = sanitizeFieldName(fieldSchema.name)
const required = fieldSchema.required ? "" : "?"
return `\t${fieldName}${required}: ${typeString}`
}
export function createSelectOptions(
recordName: string,
schema: Array<FieldSchema>
): string {
const selectFields = schema.filter((field) => field.type === "select")
const typestring = selectFields
.map(
(field) => `export enum ${getOptionEnumName(recordName, field.name)} {
${getOptionValues(field)
.map((val) => `\t"${val}" = "${val}",`)
.join("\n")}
}\n`
)
.join("\n")
return typestring
}

View File

@@ -2,79 +2,27 @@ import {
ALIAS_TYPE_DEFINITIONS,
AUTH_SYSTEM_FIELDS_DEFINITION,
BASE_SYSTEM_FIELDS_DEFINITION,
DATE_STRING_TYPE_NAME,
EXPAND_GENERIC_NAME,
EXPORT_COMMENT,
HTML_STRING_NAME,
RECORD_ID_STRING_NAME,
RECORD_TYPE_COMMENT,
RESPONSE_TYPE_COMMENT,
} from "./constants"
import { CollectionRecord, FieldSchema } from "./types"
import {
canExpand,
fieldNameToGeneric,
getGenericArgStringForRecord,
getGenericArgStringWithDefault,
} from "./generics"
import {
getOptionEnumName,
getOptionValues,
getSystemFields,
sanitizeFieldName,
toPascalCase,
} from "./utils"
import { createSelectOptions, createTypeField } from "./fields"
import { getSystemFields, toPascalCase } from "./utils"
const pbSchemaTypescriptMap = {
bool: "boolean",
date: DATE_STRING_TYPE_NAME,
editor: HTML_STRING_NAME,
email: "string",
file: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? "string[]"
: "string",
json: (fieldSchema: FieldSchema) =>
`null | ${fieldNameToGeneric(fieldSchema.name)}`,
number: "number",
relation: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect === 1
? RECORD_ID_STRING_NAME
: `${RECORD_ID_STRING_NAME}[]`,
select: (fieldSchema: FieldSchema, collectionName: string) => {
// pocketbase v0.8+ values are required
const valueType = fieldSchema.options.values
? getOptionEnumName(collectionName, fieldSchema.name)
: "string"
return fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${valueType}[]`
: valueType
},
text: "string",
url: "string",
// DEPRECATED: PocketBase v0.8 does not have a dedicated user relation
user: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${RECORD_ID_STRING_NAME}[]`
: RECORD_ID_STRING_NAME,
}
export function generate(results: Array<CollectionRecord>) {
export function generate(results: Array<CollectionRecord>): string {
const collectionNames: Array<string> = []
const recordTypes: Array<string> = []
const responseTypes: Array<string> = [RESPONSE_TYPE_COMMENT]
results
.sort((a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
})
.sort((a, b) => (a.name <= b.name ? -1 : 1))
.forEach((row) => {
if (row.name) collectionNames.push(row.name)
if (row.schema) {
@@ -99,7 +47,7 @@ export function generate(results: Array<CollectionRecord>) {
return fileParts.join("\n\n")
}
export function createCollectionEnum(collectionNames: Array<string>) {
export function createCollectionEnum(collectionNames: Array<string>): string {
const collections = collectionNames
.map((name) => `\t${toPascalCase(name)} = "${name}",`)
.join("\n")
@@ -109,7 +57,9 @@ ${collections}
return typeString
}
export function createCollectionRecords(collectionNames: Array<string>) {
export function createCollectionRecords(
collectionNames: Array<string>
): string {
const nameRecordMap = collectionNames
.map((name) => `\t${name}: ${toPascalCase(name)}Record`)
.join("\n")
@@ -136,7 +86,9 @@ ${fields}
}`
}
export function createResponseType(collectionSchemaEntry: CollectionRecord) {
export function createResponseType(
collectionSchemaEntry: CollectionRecord
): string {
const { name, schema, type } = collectionSchemaEntry
const pascaleName = toPascalCase(name)
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema, {
@@ -148,49 +100,3 @@ export function createResponseType(collectionSchemaEntry: CollectionRecord) {
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgsForRecord} & ${systemFields}${expandArgString}`
}
export function createTypeField(
collectionName: string,
fieldSchema: FieldSchema
) {
let typeStringOrFunc:
| string
| ((fieldSchema: FieldSchema, collectionName: string) => string)
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
console.log(`WARNING: unknown type "${fieldSchema.type}" found in schema`)
typeStringOrFunc = "unknown"
} else {
typeStringOrFunc =
pbSchemaTypescriptMap[
fieldSchema.type as keyof typeof pbSchemaTypescriptMap
]
}
const typeString =
typeof typeStringOrFunc === "function"
? typeStringOrFunc(fieldSchema, collectionName)
: typeStringOrFunc
const fieldName = sanitizeFieldName(fieldSchema.name)
const required = fieldSchema.required ? "" : "?"
return `\t${fieldName}${required}: ${typeString}`
}
export function createSelectOptions(
recordName: string,
schema: Array<FieldSchema>
) {
const selectFields = schema.filter((field) => field.type === "select")
const typestring = selectFields
.map(
(field) => `export enum ${getOptionEnumName(recordName, field.name)} {
${getOptionValues(field)
.map((val) => `\t"${val}" = "${val}",`)
.join("\n")}
}\n`
)
.join("\n")
return typestring
}

View File

@@ -36,8 +36,7 @@ export async function fromURL(
// Login
const { token } = await fetch(`${url}/api/admins/auth-with-password`, {
// @ts-ignore
body: formData,
body: formData,
method: "post",
}).then((res) => res.json())

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`createSelectOptions creates enum types for select options 1`] = `
"export enum ChooseTitleOptions {
"one" = "one",
"two" = "two",
"space space" = "space space",
"$@#*(&#%" = "$@#*(&#%",
}
"
`;

View File

@@ -34,16 +34,6 @@ exports[`createResponseType handles file fields with multiple files 1`] = `
}"
`;
exports[`createSelectOptions creates enum types for select options 1`] = `
"export enum ChooseTitleOptions {
"one" = "one",
"two" = "two",
"space space" = "space space",
"$@#*(&#%" = "$@#*(&#%",
}
"
`;
exports[`generate generates correct output given db input 1`] = `
"/**
* This file was @generated using pocketbase-typegen

267
test/fields.test.ts Normal file
View File

@@ -0,0 +1,267 @@
import { createSelectOptions, createTypeField } from "../src/fields"
import { FieldSchema } from "../src/types"
const defaultFieldSchema: FieldSchema = {
id: "abc",
name: "defaultName",
options: {},
required: true,
system: false,
type: "text",
unique: false,
}
describe("createTypeField", () => {
it("handles required and optional fields", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
required: false,
})
).toEqual("\tdefaultName?: string")
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
required: true,
})
).toEqual("\tdefaultName: string")
})
it("converts default types to typescript", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
})
).toEqual("\tdefaultName: string")
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "textField",
})
).toEqual("\ttextField: string")
})
it("converts number type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "numberField",
type: "number",
})
).toEqual("\tnumberField: number")
})
it("converts bool type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "boolField",
type: "bool",
})
).toEqual("\tboolField: boolean")
})
it("converts email type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "emailField",
type: "email",
})
).toEqual("\temailField: string")
})
it("converts url type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "urlField",
type: "url",
})
).toEqual("\turlField: string")
})
it("converts date type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "dateField",
type: "date",
})
).toEqual("\tdateField: IsoDateString")
})
it("converts select type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectField",
type: "select",
})
).toEqual("\tselectField: string")
})
it("converts select type with value", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectFieldWithOpts",
options: {
values: ["one", "two", "three"],
},
type: "select",
})
).toEqual(`\tselectFieldWithOpts: TestCollectionSelectFieldWithOptsOptions`)
})
it("converts multi-select type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectField",
options: {
maxSelect: 2,
},
type: "select",
})
).toEqual("\tselectField: string[]")
})
it("converts multi-select type with values", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectFieldWithOpts",
options: {
maxSelect: 2,
values: ["one", "two", "three"],
},
type: "select",
})
).toEqual(
`\tselectFieldWithOpts: TestCollectionSelectFieldWithOptsOptions[]`
)
})
it("converts json type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "jsonField",
type: "json",
})
).toEqual("\tjsonField: null | TjsonField")
})
it("converts editor type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "editorField",
type: "editor",
})
).toEqual("\teditorField: HTMLString")
})
it("converts file type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
type: "file",
})
).toEqual("\tfileField: string")
})
it("converts file type with multiple files", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
options: {
maxSelect: 3,
},
type: "file",
})
).toEqual("\tfileField: string[]")
})
it("converts relation type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationField",
type: "relation",
})
).toEqual("\trelationField: RecordIdString[]")
})
it("converts relation type with multiple options", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationFieldMany",
options: {
maxSelect: 3,
},
type: "relation",
})
).toEqual("\trelationFieldMany: RecordIdString[]")
})
it("converts relation type with unset maxSelect", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationFieldMany",
options: {
maxSelect: null,
},
type: "relation",
})
).toEqual("\trelationFieldMany: RecordIdString[]")
})
// DEPRECATED: This was removed in PocketBase v0.8
it("converts user relation type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "userRelationField",
type: "user",
})
).toEqual("\tuserRelationField: RecordIdString")
})
it("warns when encountering unexpected types", () => {
const logSpy = jest.spyOn(console, "log")
createTypeField("test_collection", {
...defaultFieldSchema,
// @ts-ignore
type: "unknowntype",
})
expect(logSpy).toHaveBeenCalledWith(
'WARNING: unknown type "unknowntype" found in schema'
)
})
})
describe("createSelectOptions", () => {
it("creates enum types for select options", () => {
const name = "choose"
const schema: FieldSchema[] = [
{
id: "hhnwjkke",
name: "title",
options: { values: ["one", "one", "two", "space space", "$@#*(&#%"] },
required: false,
system: false,
type: "select",
unique: false,
},
]
const result = createSelectOptions(name, schema)
expect(result).toMatchSnapshot()
})
})

View File

@@ -4,21 +4,9 @@ import {
createCollectionRecords,
createRecordType,
createResponseType,
createSelectOptions,
createTypeField,
generate,
} from "../src/lib"
const defaultFieldSchema: FieldSchema = {
id: "abc",
name: "defaultName",
options: {},
required: true,
system: false,
type: "text",
unique: false,
}
describe("generate", () => {
it("generates correct output given db input", () => {
const collections: Array<CollectionRecord> = [
@@ -146,257 +134,3 @@ describe("createResponseType", () => {
expect(result).toMatchSnapshot()
})
})
describe("createTypeField", () => {
it("handles required and optional fields", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
required: false,
})
).toEqual("\tdefaultName?: string")
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
required: true,
})
).toEqual("\tdefaultName: string")
})
it("converts default types to typescript", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
})
).toEqual("\tdefaultName: string")
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "textField",
})
).toEqual("\ttextField: string")
})
it("converts number type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "numberField",
type: "number",
})
).toEqual("\tnumberField: number")
})
it("converts bool type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "boolField",
type: "bool",
})
).toEqual("\tboolField: boolean")
})
it("converts email type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "emailField",
type: "email",
})
).toEqual("\temailField: string")
})
it("converts url type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "urlField",
type: "url",
})
).toEqual("\turlField: string")
})
it("converts date type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "dateField",
type: "date",
})
).toEqual("\tdateField: IsoDateString")
})
it("converts select type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectField",
type: "select",
})
).toEqual("\tselectField: string")
})
it("converts select type with value", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectFieldWithOpts",
options: {
values: ["one", "two", "three"],
},
type: "select",
})
).toEqual(`\tselectFieldWithOpts: TestCollectionSelectFieldWithOptsOptions`)
})
it("converts multi-select type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectField",
options: {
maxSelect: 2,
},
type: "select",
})
).toEqual("\tselectField: string[]")
})
it("converts multi-select type with values", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectFieldWithOpts",
options: {
maxSelect: 2,
values: ["one", "two", "three"],
},
type: "select",
})
).toEqual(
`\tselectFieldWithOpts: TestCollectionSelectFieldWithOptsOptions[]`
)
})
it("converts json type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "jsonField",
type: "json",
})
).toEqual("\tjsonField: null | TjsonField")
})
it("converts editor type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "editorField",
type: "editor",
})
).toEqual("\teditorField: HTMLString")
})
it("converts file type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
type: "file",
})
).toEqual("\tfileField: string")
})
it("converts file type with multiple files", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
options: {
maxSelect: 3,
},
type: "file",
})
).toEqual("\tfileField: string[]")
})
it("converts relation type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationField",
type: "relation",
})
).toEqual("\trelationField: RecordIdString[]")
})
it("converts relation type with multiple options", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationFieldMany",
options: {
maxSelect: 3,
},
type: "relation",
})
).toEqual("\trelationFieldMany: RecordIdString[]")
})
it("converts relation type with unset maxSelect", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationFieldMany",
options: {
maxSelect: null,
},
type: "relation",
})
).toEqual("\trelationFieldMany: RecordIdString[]")
})
// DEPRECATED: This was removed in PocketBase v0.8
it("converts user relation type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "userRelationField",
type: "user",
})
).toEqual("\tuserRelationField: RecordIdString")
})
it("warns when encountering unexpected types", () => {
const logSpy = jest.spyOn(console, "log")
createTypeField("test_collection", {
...defaultFieldSchema,
// @ts-ignore
type: "unknowntype",
})
expect(logSpy).toHaveBeenCalledWith(
'WARNING: unknown type "unknowntype" found in schema'
)
})
})
describe("createSelectOptions", () => {
it("creates enum types for select options", () => {
const name = "choose"
const schema: FieldSchema[] = [
{
id: "hhnwjkke",
name: "title",
options: { values: ["one", "one", "two", "space space", "$@#*(&#%"] },
required: false,
system: false,
type: "select",
unique: false,
},
]
const result = createSelectOptions(name, schema)
expect(result).toMatchSnapshot()
})
})