diff --git a/.eslintrc.json b/.eslintrc.json index d22699e..a07aba4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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 } + ] } } diff --git a/src/fields.ts b/src/fields.ts new file mode 100644 index 0000000..ee96d88 --- /dev/null +++ b/src/fields.ts @@ -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 +): 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 +} diff --git a/src/lib.ts b/src/lib.ts index d5d7614..8ac1e4f 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -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) { +export function generate(results: Array): string { const collectionNames: Array = [] const recordTypes: Array = [] const responseTypes: Array = [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) { return fileParts.join("\n\n") } -export function createCollectionEnum(collectionNames: Array) { +export function createCollectionEnum(collectionNames: Array): string { const collections = collectionNames .map((name) => `\t${toPascalCase(name)} = "${name}",`) .join("\n") @@ -109,7 +57,9 @@ ${collections} return typeString } -export function createCollectionRecords(collectionNames: Array) { +export function createCollectionRecords( + collectionNames: Array +): 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 -) { - 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 -} diff --git a/src/schema.ts b/src/schema.ts index 720fdd5..9c3f26c 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -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()) diff --git a/test/__snapshots__/fields.test.ts.snap b/test/__snapshots__/fields.test.ts.snap new file mode 100644 index 0000000..e2ee657 --- /dev/null +++ b/test/__snapshots__/fields.test.ts.snap @@ -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", + "$@#*(&#%" = "$@#*(&#%", +} +" +`; diff --git a/test/__snapshots__/lib.test.ts.snap b/test/__snapshots__/lib.test.ts.snap index 4486872..b63523a 100644 --- a/test/__snapshots__/lib.test.ts.snap +++ b/test/__snapshots__/lib.test.ts.snap @@ -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 diff --git a/test/fields.test.ts b/test/fields.test.ts new file mode 100644 index 0000000..73b628c --- /dev/null +++ b/test/fields.test.ts @@ -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() + }) +}) diff --git a/test/lib.test.ts b/test/lib.test.ts index cde3f9b..6fa2dce 100644 --- a/test/lib.test.ts +++ b/test/lib.test.ts @@ -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 = [ @@ -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() - }) -})