From 04b21525f3959f26e577e848866f406c0965e8c5 Mon Sep 17 00:00:00 2001 From: "Alex Rock (Koala)" Date: Fri, 27 Sep 2024 12:14:05 -0600 Subject: [PATCH 01/15] koala: initial commit --- validators/StringValidator/README.MD | 72 +++++++++++ validators/StringValidator/metadata.json | 112 +++++++++++++++++ validators/StringValidator/package.json | 64 ++++++++++ validators/StringValidator/rollup.config.mjs | 5 + validators/StringValidator/src/index.ts | 126 +++++++++++++++++++ 5 files changed, 379 insertions(+) create mode 100644 validators/StringValidator/README.MD create mode 100644 validators/StringValidator/metadata.json create mode 100644 validators/StringValidator/package.json create mode 100644 validators/StringValidator/rollup.config.mjs create mode 100644 validators/StringValidator/src/index.ts diff --git a/validators/StringValidator/README.MD b/validators/StringValidator/README.MD new file mode 100644 index 000000000..498fe7478 --- /dev/null +++ b/validators/StringValidator/README.MD @@ -0,0 +1,72 @@ +# Flatfile String Configuration Plugin + +A powerful and flexible Flatfile Listener plugin for string configuration and validation. This plugin combines multiple string validations in a single configuration, including regex pattern matching, length validation, case sensitivity, trimming options, and custom transformations. + +## Features + +- Regular expression pattern matching +- Length validation (min, max, exact) +- Case type enforcement (lowercase, uppercase, titlecase) +- Whitespace trimming (leading, trailing) +- Custom transformation functions +- Configurable error messages +- Common regex patterns for email, phone, and URL validation +- Empty string handling + +## Installation + +To install the plugin, use npm: + +```bash +npm install @flatfile/plugin-string-config +``` + +## Example Usage + +```typescript +import { FlatfileListener } from '@flatfile/listener'; +import { stringConfigPlugin } from '@flatfile/plugin-string-config'; + +const listener = new FlatfileListener(); + +const stringConfigs = { + email: { + pattern: commonRegexPatterns.email, + trim: { leading: true, trailing: true }, + errorMessages: { + pattern: 'Invalid email format', + }, + }, + name: { + minLength: 2, + maxLength: 50, + caseType: 'titlecase', + errorMessages: { + length: 'Name must be between 2 and 50 characters', + case: 'Name must be in Title Case', + }, + }, +}; + +listener.use(stringConfigPlugin(stringConfigs)); +``` + +## Configuration + +The `stringConfigPlugin` accepts a configuration object where each key represents a field name and its value is a `StringValidationConfig` object with the following properties: + +- `pattern`: RegExp - A regular expression pattern to match against +- `minLength`: number - Minimum length of the string +- `maxLength`: number - Maximum length of the string +- `exactLength`: number - Exact required length of the string +- `caseType`: 'lowercase' | 'uppercase' | 'titlecase' - Enforces specific case type +- `trim`: { leading?: boolean, trailing?: boolean } - Trims whitespace +- `customTransform`: (value: string) => string - Custom transformation function +- `emptyStringAllowed`: boolean - Whether empty strings are allowed +- `errorMessages`: Object with custom error messages for different validations + +## Behavior + +The plugin processes each record in the Flatfile import, applying the configured validations to the specified fields. If a validation fails, an error is added to the record for that field. If a custom transformation is specified and all validations pass, the transformed value is set for the field. + +The plugin uses the `recordHook` to process individual records, allowing for efficient and flexible validation and transformation of string fields during the import process. \ No newline at end of file diff --git a/validators/StringValidator/metadata.json b/validators/StringValidator/metadata.json new file mode 100644 index 000000000..39de6bfb9 --- /dev/null +++ b/validators/StringValidator/metadata.json @@ -0,0 +1,112 @@ +{ + "timestamp": "2024-09-27T16-40-59-748Z", + "task": "Create a String Config Flatfile Listener plugin:\n - Implement custom validation for string fields using configurable options\n - Support regex pattern matching with customizable error messages\n - Add length validation (min, max, exact) for string fields\n - Implement character type restrictions (e.g., alphanumeric, numeric only)\n - Add case sensitivity options (lowercase, uppercase, title case)\n - Implement trimming options for leading/trailing whitespace\n - Support custom string transformations (e.g., capitalize first letter)\n - Provide options for handling empty strings and null values\n - Implement a library of common regex patterns (email, phone, URL, etc.)\n - Allow for combining multiple string validations in a single config", + "summary": "This code implements a Flatfile Listener plugin for string configuration and validation. It combines multiple string validations in a single configuration, including regex pattern matching, length validation, case sensitivity, trimming options, and custom transformations. The plugin uses the recordHook to process individual records and apply the configured validations.", + "steps": [ + [ + "Retrieve information about Flatfile Listeners and the Record Hook plugin.\n", + "#E1", + "PineconeAssistant", + "Provide information about Flatfile Listeners and the Record Hook plugin, including their structure and usage.", + "Plan: Retrieve information about Flatfile Listeners and the Record Hook plugin.\n#E1 = PineconeAssistant[Provide information about Flatfile Listeners and the Record Hook plugin, including their structure and usage.]" + ], + [ + "Generate the basic structure of the String Config Flatfile Listener plugin.\n", + "#E2", + "PineconeAssistant", + "Generate the basic structure of a Flatfile Listener plugin for string configuration, including the necessary imports and the main listener function.", + "Plan: Generate the basic structure of the String Config Flatfile Listener plugin.\n#E2 = PineconeAssistant[Generate the basic structure of a Flatfile Listener plugin for string configuration, including the necessary imports and the main listener function.]" + ], + [ + "Implement custom validation for string fields using configurable options.\n", + "#E3", + "PineconeAssistant", + "Add custom validation logic for string fields using configurable options within the listener function from #E2.", + "Plan: Implement custom validation for string fields using configurable options.\n#E3 = PineconeAssistant[Add custom validation logic for string fields using configurable options within the listener function from #E2.]" + ], + [ + "Implement regex pattern matching with customizable error messages.\n", + "#E4", + "PineconeAssistant", + "Add regex pattern matching functionality with customizable error messages to the listener function from #E3.", + "Plan: Implement regex pattern matching with customizable error messages.\n#E4 = PineconeAssistant[Add regex pattern matching functionality with customizable error messages to the listener function from #E3.]" + ], + [ + "Add length validation (min, max, exact) for string fields.\n", + "#E5", + "PineconeAssistant", + "Implement length validation (min, max, exact) for string fields in the listener function from #E4.", + "Plan: Add length validation (min, max, exact) for string fields.\n#E5 = PineconeAssistant[Implement length validation (min, max, exact) for string fields in the listener function from #E4.]" + ], + [ + "Implement character type restrictions.\n", + "#E6", + "PineconeAssistant", + "Add character type restrictions (e.g., alphanumeric, numeric only) to the listener function from #E5.", + "Plan: Implement character type restrictions.\n#E6 = PineconeAssistant[Add character type restrictions (e.g., alphanumeric, numeric only) to the listener function from #E5.]" + ], + [ + "Add case sensitivity options.\n", + "#E7", + "PineconeAssistant", + "Implement case sensitivity options (lowercase, uppercase, title case) in the listener function from #E6.", + "Plan: Add case sensitivity options.\n#E7 = PineconeAssistant[Implement case sensitivity options (lowercase, uppercase, title case) in the listener function from #E6.]" + ], + [ + "Implement trimming options for leading/trailing whitespace.\n", + "#E8", + "PineconeAssistant", + "Add trimming options for leading/trailing whitespace to the listener function from #E7.", + "Plan: Implement trimming options for leading/trailing whitespace.\n#E8 = PineconeAssistant[Add trimming options for leading/trailing whitespace to the listener function from #E7.]" + ], + [ + "Implement custom string transformations.\n", + "#E9", + "PineconeAssistant", + "Add support for custom string transformations (e.g., capitalize first letter) to the listener function from #E8.", + "Plan: Implement custom string transformations.\n#E9 = PineconeAssistant[Add support for custom string transformations (e.g., capitalize first letter) to the listener function from #E8.]" + ], + [ + "Provide options for handling empty strings and null values.\n", + "#E10", + "PineconeAssistant", + "Implement options for handling empty strings and null values in the listener function from #E9.", + "Plan: Provide options for handling empty strings and null values.\n#E10 = PineconeAssistant[Implement options for handling empty strings and null values in the listener function from #E9.]" + ], + [ + "Create a library of common regex patterns.\n", + "#E11", + "PineconeAssistant", + "Implement a library of common regex patterns (email, phone, URL, etc.) that can be used in the listener function from #E10.", + "Plan: Create a library of common regex patterns.\n#E11 = PineconeAssistant[Implement a library of common regex patterns (email, phone, URL, etc.) that can be used in the listener function from #E10.]" + ], + [ + "Allow for combining multiple string validations in a single config.\n", + "#E12", + "PineconeAssistant", + "Modify the listener function from #E11 to support combining multiple string validations in a single configuration.", + "Plan: Allow for combining multiple string validations in a single config.\n#E12 = PineconeAssistant[Modify the listener function from #E11 to support combining multiple string validations in a single configuration.]" + ], + [ + "Review and optimize the final code.\n", + "#E13", + "PineconeAssistant", + "Review the complete String Config Flatfile Listener plugin code from #E12, optimize it if necessary, and ensure all requirements are met.", + "Plan: Review and optimize the final code.\n#E13 = PineconeAssistant[Review the complete String Config Flatfile Listener plugin code from #E12, optimize it if necessary, and ensure all requirements are met.]" + ], + [ + "Validate the final code and check for unused imports.\n", + "#E14", + "LLM", + "Review the code from #E13, check for unused imports, verify that all params are correct for any plugins used, and confirm that the listener only subscribes to valid Event Topics. Provide any necessary corrections or optimizations.", + "Plan: Validate the final code and check for unused imports.\n#E14 = LLM[Review the code from #E13, check for unused imports, verify that all params are correct for any plugins used, and confirm that the listener only subscribes to valid Event Topics. Provide any necessary corrections or optimizations.]" + ] + ], + "metrics": { + "tokens": { + "plan": 6431, + "state": 8882, + "total": 15313 + } + } +} \ No newline at end of file diff --git a/validators/StringValidator/package.json b/validators/StringValidator/package.json new file mode 100644 index 000000000..c7928deac --- /dev/null +++ b/validators/StringValidator/package.json @@ -0,0 +1,64 @@ +{ + "name": "@flatfile/plugin-validate-string", + "version": "1.0.0", + "description": "A Flatfile plugin for string configuration and validation", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "browser": { + "./dist/index.js": "./dist/index.browser.js", + "./dist/index.mjs": "./dist/index.browser.mjs" + }, + "exports": { + "types": "./dist/index.d.ts", + "node": { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "browser": { + "require": "./dist/index.browser.js", + "import": "./dist/index.browser.mjs" + }, + "default": "./dist/index.mjs" + }, + "source": "./src/index.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "rollup -c", + "build:watch": "rollup -c --watch", + "build:prod": "NODE_ENV=production rollup -c", + "check": "tsc ./**/*.ts --noEmit --esModuleInterop", + "test": "jest ./**/*.spec.ts --config=../../jest.config.js --runInBand" + }, + "keywords": [ + "flatfile", + "plugin", + "string", + "validator", + "flatfile-plugins", + "category-transform" + ], + "author": "Your Name", + "license": "MIT", + "dependencies": { + "@flatfile/plugin-record-hook": "^1.7.0" + }, + "peerDependencies": { + "@flatfile/listener": "^1.0.5" + }, + "devDependencies": { + "@flatfile/rollup-config": "^0.1.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/FlatFilers/flatfile-plugins.git", + "directory": "plugins/string-validator" + }, + "browserslist": [ + "> 0.5%", + "last 2 versions", + "not dead" + ] +} diff --git a/validators/StringValidator/rollup.config.mjs b/validators/StringValidator/rollup.config.mjs new file mode 100644 index 000000000..fafa813c6 --- /dev/null +++ b/validators/StringValidator/rollup.config.mjs @@ -0,0 +1,5 @@ +import { buildConfig } from '@flatfile/rollup-config' + +const config = buildConfig({}) + +export default config diff --git a/validators/StringValidator/src/index.ts b/validators/StringValidator/src/index.ts new file mode 100644 index 000000000..3e34c6341 --- /dev/null +++ b/validators/StringValidator/src/index.ts @@ -0,0 +1,126 @@ +import { FlatfileListener } from '@flatfile/listener' +import { recordHook } from '@flatfile/plugin-record-hook' + +interface StringValidationConfig { + pattern?: RegExp + minLength?: number + maxLength?: number + exactLength?: number + caseType?: 'lowercase' | 'uppercase' | 'titlecase' + trim?: { + leading?: boolean + trailing?: boolean + } + customTransform?: (value: string) => string + emptyStringAllowed?: boolean + errorMessages?: { + pattern?: string + length?: string + case?: string + trim?: string + } +} + +const commonRegexPatterns = { + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + phone: /^\+?[\d\s-]{10,14}$/, + url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, +} + +function validateString( + value: string, + config: StringValidationConfig +): string | null { + if (!config.emptyStringAllowed && value.trim() === '') { + return 'Field cannot be empty' + } + + if (config.pattern && !config.pattern.test(value)) { + return config.errorMessages?.pattern || 'Invalid format' + } + + if (config.minLength && value.length < config.minLength) { + return ( + config.errorMessages?.length || `Minimum length is ${config.minLength}` + ) + } + + if (config.maxLength && value.length > config.maxLength) { + return ( + config.errorMessages?.length || `Maximum length is ${config.maxLength}` + ) + } + + if (config.exactLength && value.length !== config.exactLength) { + return ( + config.errorMessages?.length || + `Exact length must be ${config.exactLength}` + ) + } + + if (config.caseType) { + let transformedValue: string + switch (config.caseType) { + case 'lowercase': + transformedValue = value.toLowerCase() + break + case 'uppercase': + transformedValue = value.toUpperCase() + break + case 'titlecase': + transformedValue = value.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) + break + } + if (value !== transformedValue) { + return ( + config.errorMessages?.case || + `Field value must be in ${config.caseType}` + ) + } + } + + if (config.trim) { + let trimmedValue = value + if (config.trim.leading) { + trimmedValue = trimmedValue.trimStart() + } + if (config.trim.trailing) { + trimmedValue = trimmedValue.trimEnd() + } + if (value !== trimmedValue) { + return ( + config.errorMessages?.trim || + 'Field value has leading or trailing whitespace' + ) + } + } + + return null +} + +export function stringConfigPlugin( + fieldConfigs: Record +) { + return (listener: FlatfileListener) => { + listener.use( + recordHook('**', (record) => { + Object.entries(fieldConfigs).forEach(([field, config]) => { + const value = record.get(field) as string + if (value !== null && value !== undefined) { + const error = validateString(value, config) + if (error) { + record.addError(field, error) + } else if (config.customTransform) { + const transformedValue = config.customTransform(value) + record.set(field, transformedValue) + } + } + }) + return record + }) + ) + } +} From c650fcf10d051a8cee4066e6d43a08fef18aa261 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 1 Oct 2024 01:47:30 -0600 Subject: [PATCH 02/15] feat: cleanup and test --- .../string}/README.MD | 54 +++-- .../string}/metadata.json | 0 .../string}/package.json | 0 .../string}/rollup.config.mjs | 0 .../string/src/ValidateString.e2e.spec.ts | 191 ++++++++++++++++++ .../string}/src/index.ts | 24 ++- 6 files changed, 239 insertions(+), 30 deletions(-) rename {validators/StringValidator => validate/string}/README.MD (61%) rename {validators/StringValidator => validate/string}/metadata.json (100%) rename {validators/StringValidator => validate/string}/package.json (100%) rename {validators/StringValidator => validate/string}/rollup.config.mjs (100%) create mode 100644 validate/string/src/ValidateString.e2e.spec.ts rename {validators/StringValidator => validate/string}/src/index.ts (81%) diff --git a/validators/StringValidator/README.MD b/validate/string/README.MD similarity index 61% rename from validators/StringValidator/README.MD rename to validate/string/README.MD index 498fe7478..0c387fe85 100644 --- a/validators/StringValidator/README.MD +++ b/validate/string/README.MD @@ -25,37 +25,49 @@ npm install @flatfile/plugin-string-config ```typescript import { FlatfileListener } from '@flatfile/listener'; -import { stringConfigPlugin } from '@flatfile/plugin-string-config'; +import { validateString } from '@flatfile/plugin-string-config'; const listener = new FlatfileListener(); -const stringConfigs = { - email: { - pattern: commonRegexPatterns.email, - trim: { leading: true, trailing: true }, - errorMessages: { - pattern: 'Invalid email format', - }, - }, - name: { - minLength: 2, - maxLength: 50, - caseType: 'titlecase', - errorMessages: { - length: 'Name must be between 2 and 50 characters', - case: 'Name must be in Title Case', - }, +const stringConfig = { + fields: ['name'], + minLength: 2, + maxLength: 50, + caseType: 'titlecase', + errorMessages: { + length: 'Name must be between 2 and 50 characters', + case: 'Name must be in Title Case', }, + }; -listener.use(stringConfigPlugin(stringConfigs)); +listener.use(validateString(stringConfig)); ``` -## Configuration +**Pattern usage:** +```typescript +const config = { + fields: ['email'], + pattern: 'email' // Uses predefined email pattern +}; -The `stringConfigPlugin` accepts a configuration object where each key represents a field name and its value is a `StringValidationConfig` object with the following properties: +// Or with a custom pattern: +const customConfig = { + fields: ['customField'], + pattern: /^[A-Z]{3}-\d{2}$/ // Custom pattern for format like 'ABC-12' +}; +``` + +## Configuration +The `validateString` accepts a `StringValidationConfig` object with the following properties: +- `fields`: string[] - Fields to validate +- `sheetSlug`: string - Sheet slug to validate (defaults to '**' all sheets) - `pattern`: RegExp - A regular expression pattern to match against +- `pattern`: keyof typeof commonRegexPatterns | RegExp - A regular expression pattern to match against. You can use one of the predefined patterns ('email', 'phone', 'url') or provide a custom RegExp. The predefined patterns are: + - `email`: Validates email addresses + - `phone`: Validates phone numbers (10-14 digits, optional '+' prefix) + - `url`: Validates URLs (with or without protocol) - `minLength`: number - Minimum length of the string - `maxLength`: number - Maximum length of the string - `exactLength`: number - Exact required length of the string @@ -69,4 +81,4 @@ The `stringConfigPlugin` accepts a configuration object where each key represent The plugin processes each record in the Flatfile import, applying the configured validations to the specified fields. If a validation fails, an error is added to the record for that field. If a custom transformation is specified and all validations pass, the transformed value is set for the field. -The plugin uses the `recordHook` to process individual records, allowing for efficient and flexible validation and transformation of string fields during the import process. \ No newline at end of file +The plugin uses the `recordHook` to process individual records, allowing for efficient and flexible validation and transformation of string fields during the import process. diff --git a/validators/StringValidator/metadata.json b/validate/string/metadata.json similarity index 100% rename from validators/StringValidator/metadata.json rename to validate/string/metadata.json diff --git a/validators/StringValidator/package.json b/validate/string/package.json similarity index 100% rename from validators/StringValidator/package.json rename to validate/string/package.json diff --git a/validators/StringValidator/rollup.config.mjs b/validate/string/rollup.config.mjs similarity index 100% rename from validators/StringValidator/rollup.config.mjs rename to validate/string/rollup.config.mjs diff --git a/validate/string/src/ValidateString.e2e.spec.ts b/validate/string/src/ValidateString.e2e.spec.ts new file mode 100644 index 000000000..11dc71cc3 --- /dev/null +++ b/validate/string/src/ValidateString.e2e.spec.ts @@ -0,0 +1,191 @@ +import { FlatfileClient } from '@flatfile/api' +import { + createRecords, + deleteSpace, + getRecords, + setupListener, + setupSimpleWorkbook, + setupSpace, +} from '@flatfile/utils-testing' +import { validateString } from './index' + +const api = new FlatfileClient() + +describe('ValidateString e2e', () => { + const listener = setupListener() + + let spaceId: string + let sheetId: string + + beforeAll(async () => { + const space = await setupSpace() + spaceId = space.id + const workbook = await setupSimpleWorkbook(space.id, [ + { key: 'name', type: 'string' }, + { key: 'email', type: 'string' }, + { key: 'code', type: 'string' }, + ]) + sheetId = workbook.sheets![0].id + }) + + afterAll(async () => { + await deleteSpace(spaceId) + }) + + afterEach(async () => { + listener.reset() + const records = await getRecords(sheetId) + if (records.length > 0) { + const ids = records.map((record) => record.id) + await api.records.delete(sheetId, { ids }) + } + }) + + describe('validateString()', () => { + it('validates string length', async () => { + listener.use( + validateString({ + sheetSlug: 'test', + fields: ['name'], + minLength: 2, + maxLength: 10, + }) + ) + + await createRecords(sheetId, [ + { name: 'A' }, + { name: 'Valid' }, + { name: 'TooLongString' }, + ]) + await listener.waitFor('commit:created') + + const records = await getRecords(sheetId) + + expect(records[0].valid).toBeFalsy() + expect(records[0].values['name']?.messages?.[0]?.message).toContain( + 'at least 2 characters' + ) + expect(records[1].valid).toBeTruthy() + expect(records[2].valid).toBeFalsy() + expect(records[2].values['name']?.messages?.[0]?.message).toContain( + 'at most 10 characters' + ) + }) + + it('validates string pattern', async () => { + listener.use( + validateString({ + sheetSlug: 'test', + fields: ['email'], + pattern: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/, + }) + ) + + await createRecords(sheetId, [ + { email: 'invalid' }, + { email: 'valid@example.com' }, + { email: 'another.valid@email.co.uk' }, + ]) + await listener.waitFor('commit:created') + + const records = await getRecords(sheetId) + + expect(records[0].valid).toBeFalsy() + expect(records[0].values['email'].messages?.[0].message).toContain( + 'match the required pattern' + ) + expect(records[1].valid).toBeTruthy() + expect(records[2].valid).toBeTruthy() + }) + + it('validates custom function', async () => { + listener.use( + validateString({ + sheetSlug: 'test', + fields: ['code'], + customTransform: (value) => { + if (!/^[A-Z]{3}-\d{3}$/.test(value)) { + return 'Code must be in the format XXX-000' + } + return value + }, + }) + ) + + await createRecords(sheetId, [ + { code: 'ABC-123' }, + { code: 'DEF-456' }, + { code: 'invalid' }, + ]) + await listener.waitFor('commit:created') + + const records = await getRecords(sheetId) + + expect(records[0].valid).toBeTruthy() + expect(records[1].valid).toBeTruthy() + expect(records[2].valid).toBeFalsy() + expect(records[2].values['code'].messages?.[0].message).toBe( + 'Code must be in the format XXX-000' + ) + }) + + it('handles multiple validation rules', async () => { + listener.use( + validateString({ + sheetSlug: 'test', + fields: ['name'], + minLength: 3, + maxLength: 20, + pattern: /^[A-Za-z\s]+$/, + customTransform: (value) => { + if (value.split(' ').length < 2) { + return 'Name must include both first and last name' + } + return value + }, + }) + ) + + await createRecords(sheetId, [ + { name: 'John Doe' }, + { name: 'A' }, + { name: 'OnlyFirstName' }, + { name: 'Contains123Numbers' }, + ]) + await listener.waitFor('commit:created') + + const records = await getRecords(sheetId) + + expect(records[0].valid).toBeTruthy() + expect(records[1].valid).toBeFalsy() + expect(records[1].values['name'].messages?.length).toBe(2) // minLength and customValidation + expect(records[2].valid).toBeFalsy() + expect(records[2].values['name'].messages?.[0].message).toContain( + 'both first and last name' + ) + expect(records[3].valid).toBeFalsy() + expect(records[3].values['name'].messages?.[0].message).toContain( + 'match the required pattern' + ) + }) + + it('handles empty strings correctly', async () => { + listener.use( + validateString({ + sheetSlug: 'test', + fields: ['name'], + minLength: 1, + emptyStringAllowed: true, + }) + ) + + await createRecords(sheetId, [{ name: '' }, { name: 'Valid' }]) + await listener.waitFor('commit:created') + + const records = await getRecords(sheetId) + + expect(records[0].valid).toBeTruthy() + expect(records[1].valid).toBeTruthy() + }) + }) +}) diff --git a/validators/StringValidator/src/index.ts b/validate/string/src/index.ts similarity index 81% rename from validators/StringValidator/src/index.ts rename to validate/string/src/index.ts index 3e34c6341..29ee74cca 100644 --- a/validators/StringValidator/src/index.ts +++ b/validate/string/src/index.ts @@ -2,7 +2,9 @@ import { FlatfileListener } from '@flatfile/listener' import { recordHook } from '@flatfile/plugin-record-hook' interface StringValidationConfig { - pattern?: RegExp + fields: string[] + sheetSlug?: string // Added sheetSlug parameter + pattern?: keyof typeof commonRegexPatterns | RegExp // Allow for common regex patterns or custom minLength?: number maxLength?: number exactLength?: number @@ -27,7 +29,7 @@ const commonRegexPatterns = { url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, } -function validateString( +function StringValidation( value: string, config: StringValidationConfig ): string | null { @@ -35,7 +37,8 @@ function validateString( return 'Field cannot be empty' } - if (config.pattern && !config.pattern.test(value)) { + const pattern = typeof config.pattern === 'string' ? commonRegexPatterns[config.pattern] : config.pattern; + if (pattern && !pattern.test(value)) { return config.errorMessages?.pattern || 'Invalid format' } @@ -101,16 +104,17 @@ function validateString( return null } -export function stringConfigPlugin( - fieldConfigs: Record + +export function validateString( + config: StringValidationConfig ) { return (listener: FlatfileListener) => { listener.use( - recordHook('**', (record) => { - Object.entries(fieldConfigs).forEach(([field, config]) => { + recordHook(config.sheetSlug || '**', (record) => { // Use sheetSlug in recordHook + for (const field of config.fields) { const value = record.get(field) as string if (value !== null && value !== undefined) { - const error = validateString(value, config) + const error = StringValidation(value, config) if (error) { record.addError(field, error) } else if (config.customTransform) { @@ -118,9 +122,11 @@ export function stringConfigPlugin( record.set(field, transformedValue) } } - }) + } return record }) ) } } + +export default validateString From 47b1a5e6b9dfc2d3c5781f2e56ee152788a39634 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 1 Oct 2024 02:37:37 -0600 Subject: [PATCH 03/15] feat: cleanup --- .../string/src/ValidateString.e2e.spec.ts | 77 +------------------ validate/string/src/index.ts | 6 +- 2 files changed, 4 insertions(+), 79 deletions(-) diff --git a/validate/string/src/ValidateString.e2e.spec.ts b/validate/string/src/ValidateString.e2e.spec.ts index 11dc71cc3..42d298562 100644 --- a/validate/string/src/ValidateString.e2e.spec.ts +++ b/validate/string/src/ValidateString.e2e.spec.ts @@ -63,12 +63,12 @@ describe('ValidateString e2e', () => { expect(records[0].valid).toBeFalsy() expect(records[0].values['name']?.messages?.[0]?.message).toContain( - 'at least 2 characters' + 'Minimum length is 2' ) expect(records[1].valid).toBeTruthy() expect(records[2].valid).toBeFalsy() expect(records[2].values['name']?.messages?.[0]?.message).toContain( - 'at most 10 characters' + 'Maximum length is 10' ) }) @@ -92,83 +92,12 @@ describe('ValidateString e2e', () => { expect(records[0].valid).toBeFalsy() expect(records[0].values['email'].messages?.[0].message).toContain( - 'match the required pattern' + 'Invalid format' ) expect(records[1].valid).toBeTruthy() expect(records[2].valid).toBeTruthy() }) - it('validates custom function', async () => { - listener.use( - validateString({ - sheetSlug: 'test', - fields: ['code'], - customTransform: (value) => { - if (!/^[A-Z]{3}-\d{3}$/.test(value)) { - return 'Code must be in the format XXX-000' - } - return value - }, - }) - ) - - await createRecords(sheetId, [ - { code: 'ABC-123' }, - { code: 'DEF-456' }, - { code: 'invalid' }, - ]) - await listener.waitFor('commit:created') - - const records = await getRecords(sheetId) - - expect(records[0].valid).toBeTruthy() - expect(records[1].valid).toBeTruthy() - expect(records[2].valid).toBeFalsy() - expect(records[2].values['code'].messages?.[0].message).toBe( - 'Code must be in the format XXX-000' - ) - }) - - it('handles multiple validation rules', async () => { - listener.use( - validateString({ - sheetSlug: 'test', - fields: ['name'], - minLength: 3, - maxLength: 20, - pattern: /^[A-Za-z\s]+$/, - customTransform: (value) => { - if (value.split(' ').length < 2) { - return 'Name must include both first and last name' - } - return value - }, - }) - ) - - await createRecords(sheetId, [ - { name: 'John Doe' }, - { name: 'A' }, - { name: 'OnlyFirstName' }, - { name: 'Contains123Numbers' }, - ]) - await listener.waitFor('commit:created') - - const records = await getRecords(sheetId) - - expect(records[0].valid).toBeTruthy() - expect(records[1].valid).toBeFalsy() - expect(records[1].values['name'].messages?.length).toBe(2) // minLength and customValidation - expect(records[2].valid).toBeFalsy() - expect(records[2].values['name'].messages?.[0].message).toContain( - 'both first and last name' - ) - expect(records[3].valid).toBeFalsy() - expect(records[3].values['name'].messages?.[0].message).toContain( - 'match the required pattern' - ) - }) - it('handles empty strings correctly', async () => { listener.use( validateString({ diff --git a/validate/string/src/index.ts b/validate/string/src/index.ts index 29ee74cca..6ab1d3e7d 100644 --- a/validate/string/src/index.ts +++ b/validate/string/src/index.ts @@ -13,7 +13,6 @@ interface StringValidationConfig { leading?: boolean trailing?: boolean } - customTransform?: (value: string) => string emptyStringAllowed?: boolean errorMessages?: { pattern?: string @@ -117,10 +116,7 @@ export function validateString( const error = StringValidation(value, config) if (error) { record.addError(field, error) - } else if (config.customTransform) { - const transformedValue = config.customTransform(value) - record.set(field, transformedValue) - } + } } } return record From f0cc2313680b6a6d40f95bf51fbbae7112feaaaf Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Thu, 3 Oct 2024 14:31:21 -0600 Subject: [PATCH 04/15] feat: Readme updates --- validate/string/README.MD | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/validate/string/README.MD b/validate/string/README.MD index 0c387fe85..8b0a0fbdb 100644 --- a/validate/string/README.MD +++ b/validate/string/README.MD @@ -1,7 +1,11 @@ -# Flatfile String Configuration Plugin + -A powerful and flexible Flatfile Listener plugin for string configuration and validation. This plugin combines multiple string validations in a single configuration, including regex pattern matching, length validation, case sensitivity, trimming options, and custom transformations. +The `@flatfile/plugin-validate-string` plugin for string configuration and validation. This plugin combines multiple string validations in a single configuration, including regex pattern matching, length validation, case sensitivity, trimming options, and custom transformations. +**Event Type:** +`listener.on('commit:created')` + + ## Features - Regular expression pattern matching @@ -18,14 +22,14 @@ A powerful and flexible Flatfile Listener plugin for string configuration and va To install the plugin, use npm: ```bash -npm install @flatfile/plugin-string-config +npm install @flatfile/plugin-validate-string ``` ## Example Usage ```typescript import { FlatfileListener } from '@flatfile/listener'; -import { validateString } from '@flatfile/plugin-string-config'; +import { validateString } from '@flatfile/plugin-validate-string'; const listener = new FlatfileListener(); @@ -73,7 +77,6 @@ The `validateString` accepts a `StringValidationConfig` object with the followin - `exactLength`: number - Exact required length of the string - `caseType`: 'lowercase' | 'uppercase' | 'titlecase' - Enforces specific case type - `trim`: { leading?: boolean, trailing?: boolean } - Trims whitespace -- `customTransform`: (value: string) => string - Custom transformation function - `emptyStringAllowed`: boolean - Whether empty strings are allowed - `errorMessages`: Object with custom error messages for different validations From 16f316e6b01b050697fceeeffcbac9442f453d9f Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:12:29 -0400 Subject: [PATCH 05/15] feat: refactor to unit tests --- .../string/src/ValidateString.e2e.spec.ts | 120 -------------- validate/string/src/index.ts | 129 +--------------- .../string/src/validate.string.plugin.spec.ts | 146 ++++++++++++++++++ validate/string/src/validate.string.plugin.ts | 127 +++++++++++++++ 4 files changed, 274 insertions(+), 248 deletions(-) delete mode 100644 validate/string/src/ValidateString.e2e.spec.ts create mode 100644 validate/string/src/validate.string.plugin.spec.ts create mode 100644 validate/string/src/validate.string.plugin.ts diff --git a/validate/string/src/ValidateString.e2e.spec.ts b/validate/string/src/ValidateString.e2e.spec.ts deleted file mode 100644 index 42d298562..000000000 --- a/validate/string/src/ValidateString.e2e.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { FlatfileClient } from '@flatfile/api' -import { - createRecords, - deleteSpace, - getRecords, - setupListener, - setupSimpleWorkbook, - setupSpace, -} from '@flatfile/utils-testing' -import { validateString } from './index' - -const api = new FlatfileClient() - -describe('ValidateString e2e', () => { - const listener = setupListener() - - let spaceId: string - let sheetId: string - - beforeAll(async () => { - const space = await setupSpace() - spaceId = space.id - const workbook = await setupSimpleWorkbook(space.id, [ - { key: 'name', type: 'string' }, - { key: 'email', type: 'string' }, - { key: 'code', type: 'string' }, - ]) - sheetId = workbook.sheets![0].id - }) - - afterAll(async () => { - await deleteSpace(spaceId) - }) - - afterEach(async () => { - listener.reset() - const records = await getRecords(sheetId) - if (records.length > 0) { - const ids = records.map((record) => record.id) - await api.records.delete(sheetId, { ids }) - } - }) - - describe('validateString()', () => { - it('validates string length', async () => { - listener.use( - validateString({ - sheetSlug: 'test', - fields: ['name'], - minLength: 2, - maxLength: 10, - }) - ) - - await createRecords(sheetId, [ - { name: 'A' }, - { name: 'Valid' }, - { name: 'TooLongString' }, - ]) - await listener.waitFor('commit:created') - - const records = await getRecords(sheetId) - - expect(records[0].valid).toBeFalsy() - expect(records[0].values['name']?.messages?.[0]?.message).toContain( - 'Minimum length is 2' - ) - expect(records[1].valid).toBeTruthy() - expect(records[2].valid).toBeFalsy() - expect(records[2].values['name']?.messages?.[0]?.message).toContain( - 'Maximum length is 10' - ) - }) - - it('validates string pattern', async () => { - listener.use( - validateString({ - sheetSlug: 'test', - fields: ['email'], - pattern: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/, - }) - ) - - await createRecords(sheetId, [ - { email: 'invalid' }, - { email: 'valid@example.com' }, - { email: 'another.valid@email.co.uk' }, - ]) - await listener.waitFor('commit:created') - - const records = await getRecords(sheetId) - - expect(records[0].valid).toBeFalsy() - expect(records[0].values['email'].messages?.[0].message).toContain( - 'Invalid format' - ) - expect(records[1].valid).toBeTruthy() - expect(records[2].valid).toBeTruthy() - }) - - it('handles empty strings correctly', async () => { - listener.use( - validateString({ - sheetSlug: 'test', - fields: ['name'], - minLength: 1, - emptyStringAllowed: true, - }) - ) - - await createRecords(sheetId, [{ name: '' }, { name: 'Valid' }]) - await listener.waitFor('commit:created') - - const records = await getRecords(sheetId) - - expect(records[0].valid).toBeTruthy() - expect(records[1].valid).toBeTruthy() - }) - }) -}) diff --git a/validate/string/src/index.ts b/validate/string/src/index.ts index 6ab1d3e7d..fbbda1503 100644 --- a/validate/string/src/index.ts +++ b/validate/string/src/index.ts @@ -1,128 +1 @@ -import { FlatfileListener } from '@flatfile/listener' -import { recordHook } from '@flatfile/plugin-record-hook' - -interface StringValidationConfig { - fields: string[] - sheetSlug?: string // Added sheetSlug parameter - pattern?: keyof typeof commonRegexPatterns | RegExp // Allow for common regex patterns or custom - minLength?: number - maxLength?: number - exactLength?: number - caseType?: 'lowercase' | 'uppercase' | 'titlecase' - trim?: { - leading?: boolean - trailing?: boolean - } - emptyStringAllowed?: boolean - errorMessages?: { - pattern?: string - length?: string - case?: string - trim?: string - } -} - -const commonRegexPatterns = { - email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - phone: /^\+?[\d\s-]{10,14}$/, - url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, -} - -function StringValidation( - value: string, - config: StringValidationConfig -): string | null { - if (!config.emptyStringAllowed && value.trim() === '') { - return 'Field cannot be empty' - } - - const pattern = typeof config.pattern === 'string' ? commonRegexPatterns[config.pattern] : config.pattern; - if (pattern && !pattern.test(value)) { - return config.errorMessages?.pattern || 'Invalid format' - } - - if (config.minLength && value.length < config.minLength) { - return ( - config.errorMessages?.length || `Minimum length is ${config.minLength}` - ) - } - - if (config.maxLength && value.length > config.maxLength) { - return ( - config.errorMessages?.length || `Maximum length is ${config.maxLength}` - ) - } - - if (config.exactLength && value.length !== config.exactLength) { - return ( - config.errorMessages?.length || - `Exact length must be ${config.exactLength}` - ) - } - - if (config.caseType) { - let transformedValue: string - switch (config.caseType) { - case 'lowercase': - transformedValue = value.toLowerCase() - break - case 'uppercase': - transformedValue = value.toUpperCase() - break - case 'titlecase': - transformedValue = value.replace( - /\w\S*/g, - (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() - ) - break - } - if (value !== transformedValue) { - return ( - config.errorMessages?.case || - `Field value must be in ${config.caseType}` - ) - } - } - - if (config.trim) { - let trimmedValue = value - if (config.trim.leading) { - trimmedValue = trimmedValue.trimStart() - } - if (config.trim.trailing) { - trimmedValue = trimmedValue.trimEnd() - } - if (value !== trimmedValue) { - return ( - config.errorMessages?.trim || - 'Field value has leading or trailing whitespace' - ) - } - } - - return null -} - - -export function validateString( - config: StringValidationConfig -) { - return (listener: FlatfileListener) => { - listener.use( - recordHook(config.sheetSlug || '**', (record) => { // Use sheetSlug in recordHook - for (const field of config.fields) { - const value = record.get(field) as string - if (value !== null && value !== undefined) { - const error = StringValidation(value, config) - if (error) { - record.addError(field, error) - } - } - } - return record - }) - ) - } -} - -export default validateString +export * from './validate.string.plugin' diff --git a/validate/string/src/validate.string.plugin.spec.ts b/validate/string/src/validate.string.plugin.spec.ts new file mode 100644 index 000000000..4199fc988 --- /dev/null +++ b/validate/string/src/validate.string.plugin.spec.ts @@ -0,0 +1,146 @@ +import { + validateAndTransformString, + StringValidationConfig, +} from './validate.string.plugin' + +describe('validateAndTransformString', () => { + it('should validate empty string', () => { + const config: StringValidationConfig = { + fields: ['test'], + emptyStringAllowed: false, + } + const result = validateAndTransformString('', config) + expect(result.error).toBe('Field cannot be empty') + }) + + it('should allow empty string when configured', () => { + const config: StringValidationConfig = { + fields: ['test'], + emptyStringAllowed: true, + } + const result = validateAndTransformString('', config) + expect(result.error).toBeNull() + }) + + it('should validate pattern', () => { + const config: StringValidationConfig = { + fields: ['test'], + pattern: 'email', + } + const validResult = validateAndTransformString('test@example.com', config) + expect(validResult.error).toBeNull() + + const invalidResult = validateAndTransformString('not-an-email', config) + expect(invalidResult.error).toBe('Invalid format') + }) + + it('should validate custom pattern', () => { + const config: StringValidationConfig = { + fields: ['test'], + pattern: /^[A-Z]{3}$/, + } + const validResult = validateAndTransformString('ABC', config) + expect(validResult.error).toBeNull() + + const invalidResult = validateAndTransformString('ABCD', config) + expect(invalidResult.error).toBe('Invalid format') + }) + + it('should validate minLength', () => { + const config: StringValidationConfig = { + fields: ['test'], + minLength: 3, + } + const validResult = validateAndTransformString('abc', config) + expect(validResult.error).toBeNull() + + const invalidResult = validateAndTransformString('ab', config) + expect(invalidResult.error).toBe('Minimum length is 3') + }) + + it('should validate maxLength', () => { + const config: StringValidationConfig = { + fields: ['test'], + maxLength: 5, + } + const validResult = validateAndTransformString('abcde', config) + expect(validResult.error).toBeNull() + + const invalidResult = validateAndTransformString('abcdef', config) + expect(invalidResult.error).toBe('Maximum length is 5') + }) + + it('should validate exactLength', () => { + const config: StringValidationConfig = { + fields: ['test'], + exactLength: 4, + } + const validResult = validateAndTransformString('abcd', config) + expect(validResult.error).toBeNull() + + const invalidResult = validateAndTransformString('abc', config) + expect(invalidResult.error).toBe('Exact length must be 4') + }) + + it('should transform to lowercase', () => { + const config: StringValidationConfig = { + fields: ['test'], + caseType: 'lowercase', + } + const result = validateAndTransformString('ABC', config) + expect(result.value).toBe('abc') + expect(result.error).toBe('Field value must be in lowercase') + }) + + it('should transform to uppercase', () => { + const config: StringValidationConfig = { + fields: ['test'], + caseType: 'uppercase', + } + const result = validateAndTransformString('abc', config) + expect(result.value).toBe('ABC') + expect(result.error).toBe('Field value must be in uppercase') + }) + + it('should transform to titlecase', () => { + const config: StringValidationConfig = { + fields: ['test'], + caseType: 'titlecase', + } + const result = validateAndTransformString('hello world', config) + expect(result.value).toBe('Hello World') + expect(result.error).toBe('Field value must be in titlecase') + }) + + it('should trim leading whitespace', () => { + const config: StringValidationConfig = { + fields: ['test'], + trim: { leading: true }, + } + const result = validateAndTransformString(' abc', config) + expect(result.value).toBe('abc') + expect(result.error).toBe('Field value has leading or trailing whitespace') + }) + + it('should trim trailing whitespace', () => { + const config: StringValidationConfig = { + fields: ['test'], + trim: { trailing: true }, + } + const result = validateAndTransformString('abc ', config) + expect(result.value).toBe('abc') + expect(result.error).toBe('Field value has leading or trailing whitespace') + }) + + it('should use custom error messages', () => { + const config: StringValidationConfig = { + fields: ['test'], + pattern: 'email', + errorMessages: { + pattern: 'Custom pattern error', + }, + } + const result = validateAndTransformString('not-an-email', config) + expect(result.error).toBe('Custom pattern error') + }) +}) diff --git a/validate/string/src/validate.string.plugin.ts b/validate/string/src/validate.string.plugin.ts new file mode 100644 index 000000000..c2c53b329 --- /dev/null +++ b/validate/string/src/validate.string.plugin.ts @@ -0,0 +1,127 @@ +import { FlatfileListener } from '@flatfile/listener' +import { recordHook } from '@flatfile/plugin-record-hook' + +export interface StringValidationConfig { + fields: string[] + sheetSlug?: string + pattern?: keyof typeof commonRegexPatterns | RegExp + minLength?: number + maxLength?: number + exactLength?: number + caseType?: 'lowercase' | 'uppercase' | 'titlecase' + trim?: { + leading?: boolean + trailing?: boolean + } + emptyStringAllowed?: boolean + errorMessages?: { + pattern?: string + length?: string + case?: string + trim?: string + } +} + +export const commonRegexPatterns = { + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + phone: /^\+?[\d\s-]{10,14}$/, + url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, +} + +export interface ValidationResult { + value: string; + error: string | null; +} + +export function validateAndTransformString( + value: string, + config: StringValidationConfig +): ValidationResult { + let transformedValue = value; + let error: string | null = null; + + if (!config.emptyStringAllowed && value.trim() === '') { + return { value, error: 'Field cannot be empty' }; + } + + const pattern = typeof config.pattern === 'string' ? commonRegexPatterns[config.pattern] : config.pattern; + if (pattern && !pattern.test(value)) { + error = config.errorMessages?.pattern || 'Invalid format'; + return { value, error }; + } + + if (config.minLength && value.length < config.minLength) { + error = config.errorMessages?.length || `Minimum length is ${config.minLength}`; + return { value, error }; + } + + if (config.maxLength && value.length > config.maxLength) { + error = config.errorMessages?.length || `Maximum length is ${config.maxLength}`; + return { value, error }; + } + + if (config.exactLength && value.length !== config.exactLength) { + error = config.errorMessages?.length || `Exact length must be ${config.exactLength}`; + return { value, error }; + } + + if (config.caseType) { + switch (config.caseType) { + case 'lowercase': + transformedValue = value.toLowerCase(); + break; + case 'uppercase': + transformedValue = value.toUpperCase(); + break; + case 'titlecase': + transformedValue = value.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ); + break; + } + if (value !== transformedValue) { + error = config.errorMessages?.case || `Field value must be in ${config.caseType}`; + return { value: transformedValue, error }; + } + } + + if (config.trim) { + if (config.trim.leading) { + transformedValue = transformedValue.trimStart(); + } + if (config.trim.trailing) { + transformedValue = transformedValue.trimEnd(); + } + if (value !== transformedValue) { + error = config.errorMessages?.trim || 'Field value has leading or trailing whitespace'; + return { value: transformedValue, error }; + } + } + + return { value: transformedValue, error: null }; +} + +export function validateString(config: StringValidationConfig) { + return (listener: FlatfileListener) => { + listener.use( + recordHook(config.sheetSlug || '**', (record) => { + for (const field of config.fields) { + const value = record.get(field) as string; + if (value !== null && value !== undefined) { + const { value: newValue, error } = validateAndTransformString(value, config); + if (error) { + record.addError(field, error); + } + if (newValue !== value) { + record.set(field, newValue); + } + } + } + return record; + }) + ); + }; +} + +export default validateString; From 3c1207cb73e9db34e0c9367a645e4abe94f9c507 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:16:49 -0400 Subject: [PATCH 06/15] feat: update package.json --- validate/string/package.json | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/validate/string/package.json b/validate/string/package.json index c7928deac..61a6601fb 100644 --- a/validate/string/package.json +++ b/validate/string/package.json @@ -1,7 +1,8 @@ { "name": "@flatfile/plugin-validate-string", - "version": "1.0.0", + "version": "0.0.1", "description": "A Flatfile plugin for string configuration and validation", + "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/plugins/record-hook", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", @@ -33,15 +34,11 @@ "test": "jest ./**/*.spec.ts --config=../../jest.config.js --runInBand" }, "keywords": [ - "flatfile", - "plugin", - "string", - "validator", "flatfile-plugins", "category-transform" ], - "author": "Your Name", - "license": "MIT", + "author": "Flatfile, Inc", + "license": "ISC", "dependencies": { "@flatfile/plugin-record-hook": "^1.7.0" }, @@ -54,7 +51,7 @@ "repository": { "type": "git", "url": "https://github.com/FlatFilers/flatfile-plugins.git", - "directory": "plugins/string-validator" + "directory": "validate/string" }, "browserslist": [ "> 0.5%", From 27b8cba0720e89fd6e8a2f67e45bddf04567d253 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:32:35 -0400 Subject: [PATCH 07/15] chore: remove metadata.json --- validate/string/metadata.json | 112 ---------------------------------- 1 file changed, 112 deletions(-) delete mode 100644 validate/string/metadata.json diff --git a/validate/string/metadata.json b/validate/string/metadata.json deleted file mode 100644 index 39de6bfb9..000000000 --- a/validate/string/metadata.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "timestamp": "2024-09-27T16-40-59-748Z", - "task": "Create a String Config Flatfile Listener plugin:\n - Implement custom validation for string fields using configurable options\n - Support regex pattern matching with customizable error messages\n - Add length validation (min, max, exact) for string fields\n - Implement character type restrictions (e.g., alphanumeric, numeric only)\n - Add case sensitivity options (lowercase, uppercase, title case)\n - Implement trimming options for leading/trailing whitespace\n - Support custom string transformations (e.g., capitalize first letter)\n - Provide options for handling empty strings and null values\n - Implement a library of common regex patterns (email, phone, URL, etc.)\n - Allow for combining multiple string validations in a single config", - "summary": "This code implements a Flatfile Listener plugin for string configuration and validation. It combines multiple string validations in a single configuration, including regex pattern matching, length validation, case sensitivity, trimming options, and custom transformations. The plugin uses the recordHook to process individual records and apply the configured validations.", - "steps": [ - [ - "Retrieve information about Flatfile Listeners and the Record Hook plugin.\n", - "#E1", - "PineconeAssistant", - "Provide information about Flatfile Listeners and the Record Hook plugin, including their structure and usage.", - "Plan: Retrieve information about Flatfile Listeners and the Record Hook plugin.\n#E1 = PineconeAssistant[Provide information about Flatfile Listeners and the Record Hook plugin, including their structure and usage.]" - ], - [ - "Generate the basic structure of the String Config Flatfile Listener plugin.\n", - "#E2", - "PineconeAssistant", - "Generate the basic structure of a Flatfile Listener plugin for string configuration, including the necessary imports and the main listener function.", - "Plan: Generate the basic structure of the String Config Flatfile Listener plugin.\n#E2 = PineconeAssistant[Generate the basic structure of a Flatfile Listener plugin for string configuration, including the necessary imports and the main listener function.]" - ], - [ - "Implement custom validation for string fields using configurable options.\n", - "#E3", - "PineconeAssistant", - "Add custom validation logic for string fields using configurable options within the listener function from #E2.", - "Plan: Implement custom validation for string fields using configurable options.\n#E3 = PineconeAssistant[Add custom validation logic for string fields using configurable options within the listener function from #E2.]" - ], - [ - "Implement regex pattern matching with customizable error messages.\n", - "#E4", - "PineconeAssistant", - "Add regex pattern matching functionality with customizable error messages to the listener function from #E3.", - "Plan: Implement regex pattern matching with customizable error messages.\n#E4 = PineconeAssistant[Add regex pattern matching functionality with customizable error messages to the listener function from #E3.]" - ], - [ - "Add length validation (min, max, exact) for string fields.\n", - "#E5", - "PineconeAssistant", - "Implement length validation (min, max, exact) for string fields in the listener function from #E4.", - "Plan: Add length validation (min, max, exact) for string fields.\n#E5 = PineconeAssistant[Implement length validation (min, max, exact) for string fields in the listener function from #E4.]" - ], - [ - "Implement character type restrictions.\n", - "#E6", - "PineconeAssistant", - "Add character type restrictions (e.g., alphanumeric, numeric only) to the listener function from #E5.", - "Plan: Implement character type restrictions.\n#E6 = PineconeAssistant[Add character type restrictions (e.g., alphanumeric, numeric only) to the listener function from #E5.]" - ], - [ - "Add case sensitivity options.\n", - "#E7", - "PineconeAssistant", - "Implement case sensitivity options (lowercase, uppercase, title case) in the listener function from #E6.", - "Plan: Add case sensitivity options.\n#E7 = PineconeAssistant[Implement case sensitivity options (lowercase, uppercase, title case) in the listener function from #E6.]" - ], - [ - "Implement trimming options for leading/trailing whitespace.\n", - "#E8", - "PineconeAssistant", - "Add trimming options for leading/trailing whitespace to the listener function from #E7.", - "Plan: Implement trimming options for leading/trailing whitespace.\n#E8 = PineconeAssistant[Add trimming options for leading/trailing whitespace to the listener function from #E7.]" - ], - [ - "Implement custom string transformations.\n", - "#E9", - "PineconeAssistant", - "Add support for custom string transformations (e.g., capitalize first letter) to the listener function from #E8.", - "Plan: Implement custom string transformations.\n#E9 = PineconeAssistant[Add support for custom string transformations (e.g., capitalize first letter) to the listener function from #E8.]" - ], - [ - "Provide options for handling empty strings and null values.\n", - "#E10", - "PineconeAssistant", - "Implement options for handling empty strings and null values in the listener function from #E9.", - "Plan: Provide options for handling empty strings and null values.\n#E10 = PineconeAssistant[Implement options for handling empty strings and null values in the listener function from #E9.]" - ], - [ - "Create a library of common regex patterns.\n", - "#E11", - "PineconeAssistant", - "Implement a library of common regex patterns (email, phone, URL, etc.) that can be used in the listener function from #E10.", - "Plan: Create a library of common regex patterns.\n#E11 = PineconeAssistant[Implement a library of common regex patterns (email, phone, URL, etc.) that can be used in the listener function from #E10.]" - ], - [ - "Allow for combining multiple string validations in a single config.\n", - "#E12", - "PineconeAssistant", - "Modify the listener function from #E11 to support combining multiple string validations in a single configuration.", - "Plan: Allow for combining multiple string validations in a single config.\n#E12 = PineconeAssistant[Modify the listener function from #E11 to support combining multiple string validations in a single configuration.]" - ], - [ - "Review and optimize the final code.\n", - "#E13", - "PineconeAssistant", - "Review the complete String Config Flatfile Listener plugin code from #E12, optimize it if necessary, and ensure all requirements are met.", - "Plan: Review and optimize the final code.\n#E13 = PineconeAssistant[Review the complete String Config Flatfile Listener plugin code from #E12, optimize it if necessary, and ensure all requirements are met.]" - ], - [ - "Validate the final code and check for unused imports.\n", - "#E14", - "LLM", - "Review the code from #E13, check for unused imports, verify that all params are correct for any plugins used, and confirm that the listener only subscribes to valid Event Topics. Provide any necessary corrections or optimizations.", - "Plan: Validate the final code and check for unused imports.\n#E14 = LLM[Review the code from #E13, check for unused imports, verify that all params are correct for any plugins used, and confirm that the listener only subscribes to valid Event Topics. Provide any necessary corrections or optimizations.]" - ] - ], - "metrics": { - "tokens": { - "plan": 6431, - "state": 8882, - "total": 15313 - } - } -} \ No newline at end of file From 9addc9cd83a8bcb1fde6762547a559597ac1e677 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:38:25 -0400 Subject: [PATCH 08/15] feat: update test scripts --- validate/phone/package.json | 4 +++- validate/string/package.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/validate/phone/package.json b/validate/phone/package.json index 053e4d5dc..fa74a2424 100644 --- a/validate/phone/package.json +++ b/validate/phone/package.json @@ -42,7 +42,9 @@ "build:watch": "rollup -c --watch", "build:prod": "NODE_ENV=production rollup -c", "check": "tsc ./**/*.ts --noEmit --esModuleInterop", - "test": "jest ./**/*.spec.ts --config=../../jest.config.js --runInBand" + "test": "jest src/*.spec.ts --detectOpenHandles", + "test:unit": "jest src/*.spec.ts --testPathIgnorePatterns=.*\\.e2e\\.spec\\.ts$ --detectOpenHandles", + "test:e2e": "jest src/*.e2e.spec.ts --detectOpenHandles" }, "keywords": [ "flatfile-plugins", diff --git a/validate/string/package.json b/validate/string/package.json index 61a6601fb..5333640c2 100644 --- a/validate/string/package.json +++ b/validate/string/package.json @@ -31,7 +31,9 @@ "build:watch": "rollup -c --watch", "build:prod": "NODE_ENV=production rollup -c", "check": "tsc ./**/*.ts --noEmit --esModuleInterop", - "test": "jest ./**/*.spec.ts --config=../../jest.config.js --runInBand" + "test": "jest src/*.spec.ts --detectOpenHandles", + "test:unit": "jest src/*.spec.ts --testPathIgnorePatterns=.*\\.e2e\\.spec\\.ts$ --detectOpenHandles", + "test:e2e": "jest src/*.e2e.spec.ts --detectOpenHandles" }, "keywords": [ "flatfile-plugins", From 5e0a5a35eede1a7cdd798f9e692628f1bad03544 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:43:52 -0400 Subject: [PATCH 09/15] feat: install deps --- package-lock.json | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f892498f..b3605ae33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3188,6 +3188,10 @@ "resolved": "validate/phone", "link": true }, + "node_modules/@flatfile/plugin-validate-string": { + "resolved": "validate/string", + "link": true + }, "node_modules/@flatfile/plugin-view-mapped": { "resolved": "plugins/view-mapped", "link": true @@ -20905,14 +20909,17 @@ }, "validate/number": { "name": "@flatfile/plugin-validate-number", - "version": "1.0.0", - "license": "MIT", + "version": "0.0.0", + "license": "ISC", "dependencies": { "@flatfile/plugin-record-hook": "^1.7.0" }, "devDependencies": { "@flatfile/rollup-config": "^0.1.1" }, + "engines": { + "node": ">= 16" + }, "peerDependencies": { "@flatfile/listener": "^1.0.5" } @@ -20934,6 +20941,19 @@ "peerDependencies": { "@flatfile/listener": "^1.0.5" } + }, + "validate/string": { + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "@flatfile/plugin-record-hook": "^1.7.0" + }, + "devDependencies": { + "@flatfile/rollup-config": "^0.1.1" + }, + "peerDependencies": { + "@flatfile/listener": "^1.0.5" + } } } } From f7c03037d40821c246092076dfc7dfd5f2cbd0a1 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:58:02 -0400 Subject: [PATCH 10/15] feat: jest.config.js --- validate/string/jest.config.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 validate/string/jest.config.js diff --git a/validate/string/jest.config.js b/validate/string/jest.config.js new file mode 100644 index 000000000..e6d7ca40b --- /dev/null +++ b/validate/string/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'node', + + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + setupFiles: ['../../test/dotenv-config.js'], + setupFilesAfterEnv: [ + '../../test/betterConsoleLog.js', + '../../test/unit.cleanup.js', + ], + testTimeout: 60_000, + globalSetup: '../../test/setup-global.js', + forceExit: true, + passWithNoTests: true, +} From 2d94a9ff250434682b956526b49e610050bbdef5 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 17:58:49 -0400 Subject: [PATCH 11/15] feat: jest.config.js --- validate/phone/jest.config.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 validate/phone/jest.config.js diff --git a/validate/phone/jest.config.js b/validate/phone/jest.config.js new file mode 100644 index 000000000..e6d7ca40b --- /dev/null +++ b/validate/phone/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'node', + + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + setupFiles: ['../../test/dotenv-config.js'], + setupFilesAfterEnv: [ + '../../test/betterConsoleLog.js', + '../../test/unit.cleanup.js', + ], + testTimeout: 60_000, + globalSetup: '../../test/setup-global.js', + forceExit: true, + passWithNoTests: true, +} From 6b696afa4708dbfe02f461d13e99320902608ecb Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Tue, 8 Oct 2024 18:16:56 -0400 Subject: [PATCH 12/15] feat: fix phone test --- validate/phone/src/validate.phone.utils.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/validate/phone/src/validate.phone.utils.spec.ts b/validate/phone/src/validate.phone.utils.spec.ts index 8a47185e4..771527b7f 100644 --- a/validate/phone/src/validate.phone.utils.spec.ts +++ b/validate/phone/src/validate.phone.utils.spec.ts @@ -3,7 +3,7 @@ import { NumberFormat } from 'libphonenumber-js'; describe('formatPhoneNumber', () => { it('should format a valid US phone number', () => { - const result = formatPhoneNumber('2125551234', 'US', NumberFormat.NATIONAL); + const result = formatPhoneNumber('2125551234', 'US', 'NATIONAL'); expect(result).toEqual({ formattedPhone: '(212) 555-1234', error: null, @@ -11,7 +11,7 @@ describe('formatPhoneNumber', () => { }); it('should format a valid UK phone number', () => { - const result = formatPhoneNumber('2071234567', 'GB', NumberFormat.INTERNATIONAL); + const result = formatPhoneNumber('2071234567', 'GB', 'INTERNATIONAL'); expect(result).toEqual({ formattedPhone: '+44 20 7123 4567', error: null, @@ -19,7 +19,7 @@ describe('formatPhoneNumber', () => { }); it('should return an error for an invalid phone number', () => { - const result = formatPhoneNumber('1234', 'US', NumberFormat.NATIONAL); + const result = formatPhoneNumber('1234', 'US', 'NATIONAL'); expect(result).toEqual({ formattedPhone: '1234', error: 'Invalid phone number format for US', @@ -27,15 +27,15 @@ describe('formatPhoneNumber', () => { }); it('should handle different number formats', () => { - const result = formatPhoneNumber('2125551234', 'US', NumberFormat.E164); + const result = formatPhoneNumber('2125551234', 'US', 'E.164'); expect(result).toEqual({ formattedPhone: '+12125551234', error: null, }); }); - it('should handle format options', () => { - const result = formatPhoneNumber('2125551234', 'US', NumberFormat.INTERNATIONAL, { formatExtension: 'national' }); + it('should handle format options with a valid fromCountry', () => { + const result = formatPhoneNumber('2125551234', 'US', 'INTERNATIONAL', { fromCountry: 'GB' }); expect(result).toEqual({ formattedPhone: '+1 212 555 1234', error: null, @@ -43,10 +43,10 @@ describe('formatPhoneNumber', () => { }); it('should return an error for an invalid country code', () => { - const result = formatPhoneNumber('2125551234', 'XX', NumberFormat.NATIONAL); + const result = formatPhoneNumber('2125551234', 'XX', 'NATIONAL'); expect(result).toEqual({ formattedPhone: '2125551234', - error: 'Error processing phone number', + error: 'Invalid phone number format for XX', }); }); }); From ad43001a5588d41064131af032872dbaa979bb45 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Wed, 9 Oct 2024 15:10:59 -0400 Subject: [PATCH 13/15] Apply suggestions from code review Co-authored-by: Carl Brugger --- validate/string/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/validate/string/package.json b/validate/string/package.json index 5333640c2..a0ab8c2d7 100644 --- a/validate/string/package.json +++ b/validate/string/package.json @@ -2,22 +2,22 @@ "name": "@flatfile/plugin-validate-string", "version": "0.0.1", "description": "A Flatfile plugin for string configuration and validation", - "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/plugins/record-hook", - "main": "./dist/index.js", + "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/validate/string", + "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "browser": { - "./dist/index.js": "./dist/index.browser.js", + "./dist/index.cjs": "./dist/index.browser.cjs", "./dist/index.mjs": "./dist/index.browser.mjs" }, "exports": { "types": "./dist/index.d.ts", "node": { "import": "./dist/index.mjs", - "require": "./dist/index.js" + "require": "./dist/index.cjs" }, "browser": { - "require": "./dist/index.browser.js", + "require": "./dist/index.browser.cjs", "import": "./dist/index.browser.mjs" }, "default": "./dist/index.mjs" From fcf4a0a322297c97fd0a7299f889207834e4689d Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Wed, 9 Oct 2024 15:27:45 -0400 Subject: [PATCH 14/15] feat: move some logic into utils file --- validate/string/src/index.ts | 1 + .../string/src/validate.string.plugin.spec.ts | 5 +- validate/string/src/validate.string.plugin.ts | 124 +++--------------- validate/string/src/validate.string.utils.ts | 110 ++++++++++++++++ 4 files changed, 127 insertions(+), 113 deletions(-) create mode 100644 validate/string/src/validate.string.utils.ts diff --git a/validate/string/src/index.ts b/validate/string/src/index.ts index fbbda1503..caf5636e4 100644 --- a/validate/string/src/index.ts +++ b/validate/string/src/index.ts @@ -1 +1,2 @@ export * from './validate.string.plugin' +export * from './validate.string.utils' diff --git a/validate/string/src/validate.string.plugin.spec.ts b/validate/string/src/validate.string.plugin.spec.ts index 4199fc988..1573a0f25 100644 --- a/validate/string/src/validate.string.plugin.spec.ts +++ b/validate/string/src/validate.string.plugin.spec.ts @@ -1,7 +1,4 @@ -import { - validateAndTransformString, - StringValidationConfig, -} from './validate.string.plugin' +import { validateAndTransformString, StringValidationConfig } from '.' describe('validateAndTransformString', () => { it('should validate empty string', () => { diff --git a/validate/string/src/validate.string.plugin.ts b/validate/string/src/validate.string.plugin.ts index c2c53b329..fde1a9ddb 100644 --- a/validate/string/src/validate.string.plugin.ts +++ b/validate/string/src/validate.string.plugin.ts @@ -1,127 +1,33 @@ import { FlatfileListener } from '@flatfile/listener' import { recordHook } from '@flatfile/plugin-record-hook' - -export interface StringValidationConfig { - fields: string[] - sheetSlug?: string - pattern?: keyof typeof commonRegexPatterns | RegExp - minLength?: number - maxLength?: number - exactLength?: number - caseType?: 'lowercase' | 'uppercase' | 'titlecase' - trim?: { - leading?: boolean - trailing?: boolean - } - emptyStringAllowed?: boolean - errorMessages?: { - pattern?: string - length?: string - case?: string - trim?: string - } -} - -export const commonRegexPatterns = { - email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - phone: /^\+?[\d\s-]{10,14}$/, - url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, -} - -export interface ValidationResult { - value: string; - error: string | null; -} - -export function validateAndTransformString( - value: string, - config: StringValidationConfig -): ValidationResult { - let transformedValue = value; - let error: string | null = null; - - if (!config.emptyStringAllowed && value.trim() === '') { - return { value, error: 'Field cannot be empty' }; - } - - const pattern = typeof config.pattern === 'string' ? commonRegexPatterns[config.pattern] : config.pattern; - if (pattern && !pattern.test(value)) { - error = config.errorMessages?.pattern || 'Invalid format'; - return { value, error }; - } - - if (config.minLength && value.length < config.minLength) { - error = config.errorMessages?.length || `Minimum length is ${config.minLength}`; - return { value, error }; - } - - if (config.maxLength && value.length > config.maxLength) { - error = config.errorMessages?.length || `Maximum length is ${config.maxLength}`; - return { value, error }; - } - - if (config.exactLength && value.length !== config.exactLength) { - error = config.errorMessages?.length || `Exact length must be ${config.exactLength}`; - return { value, error }; - } - - if (config.caseType) { - switch (config.caseType) { - case 'lowercase': - transformedValue = value.toLowerCase(); - break; - case 'uppercase': - transformedValue = value.toUpperCase(); - break; - case 'titlecase': - transformedValue = value.replace( - /\w\S*/g, - (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() - ); - break; - } - if (value !== transformedValue) { - error = config.errorMessages?.case || `Field value must be in ${config.caseType}`; - return { value: transformedValue, error }; - } - } - - if (config.trim) { - if (config.trim.leading) { - transformedValue = transformedValue.trimStart(); - } - if (config.trim.trailing) { - transformedValue = transformedValue.trimEnd(); - } - if (value !== transformedValue) { - error = config.errorMessages?.trim || 'Field value has leading or trailing whitespace'; - return { value: transformedValue, error }; - } - } - - return { value: transformedValue, error: null }; -} +import { + StringValidationConfig, + validateAndTransformString, +} from './validate.string.utils' export function validateString(config: StringValidationConfig) { return (listener: FlatfileListener) => { listener.use( recordHook(config.sheetSlug || '**', (record) => { for (const field of config.fields) { - const value = record.get(field) as string; + const value = record.get(field) as string if (value !== null && value !== undefined) { - const { value: newValue, error } = validateAndTransformString(value, config); + const { value: newValue, error } = validateAndTransformString( + value, + config + ) if (error) { - record.addError(field, error); + record.addError(field, error) } if (newValue !== value) { - record.set(field, newValue); + record.set(field, newValue) } } } - return record; + return record }) - ); - }; + ) + } } -export default validateString; +export default validateString diff --git a/validate/string/src/validate.string.utils.ts b/validate/string/src/validate.string.utils.ts new file mode 100644 index 000000000..44ff28431 --- /dev/null +++ b/validate/string/src/validate.string.utils.ts @@ -0,0 +1,110 @@ +export interface StringValidationConfig { + fields: string[] + sheetSlug?: string + pattern?: keyof typeof commonRegexPatterns | RegExp + minLength?: number + maxLength?: number + exactLength?: number + caseType?: 'lowercase' | 'uppercase' | 'titlecase' + trim?: { + leading?: boolean + trailing?: boolean + } + emptyStringAllowed?: boolean + errorMessages?: { + pattern?: string + length?: string + case?: string + trim?: string + } +} +export const commonRegexPatterns = { + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + phone: /^\+?[\d\s-]{10,14}$/, + url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, +} + +export interface ValidationResult { + value: string + error: string | null +} + +export function validateAndTransformString( + value: string, + config: StringValidationConfig +): ValidationResult { + let transformedValue = value + let error: string | null = null + + if (!config.emptyStringAllowed && value.trim() === '') { + return { value, error: 'Field cannot be empty' } + } + + const pattern = + typeof config.pattern === 'string' + ? commonRegexPatterns[config.pattern] + : config.pattern + if (pattern && !pattern.test(value)) { + error = config.errorMessages?.pattern || 'Invalid format' + return { value, error } + } + + if (config.minLength && value.length < config.minLength) { + error = + config.errorMessages?.length || `Minimum length is ${config.minLength}` + return { value, error } + } + + if (config.maxLength && value.length > config.maxLength) { + error = + config.errorMessages?.length || `Maximum length is ${config.maxLength}` + return { value, error } + } + + if (config.exactLength && value.length !== config.exactLength) { + error = + config.errorMessages?.length || + `Exact length must be ${config.exactLength}` + return { value, error } + } + + if (config.caseType) { + switch (config.caseType) { + case 'lowercase': + transformedValue = value.toLowerCase() + break + case 'uppercase': + transformedValue = value.toUpperCase() + break + case 'titlecase': + transformedValue = value.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) + break + } + if (value !== transformedValue) { + error = + config.errorMessages?.case || + `Field value must be in ${config.caseType}` + return { value: transformedValue, error } + } + } + + if (config.trim) { + if (config.trim.leading) { + transformedValue = transformedValue.trimStart() + } + if (config.trim.trailing) { + transformedValue = transformedValue.trimEnd() + } + if (value !== transformedValue) { + error = + config.errorMessages?.trim || + 'Field value has leading or trailing whitespace' + return { value: transformedValue, error } + } + } + + return { value: transformedValue, error: null } +} From f2b294c25c6b59999ac422066dfb416105706742 Mon Sep 17 00:00:00 2001 From: Alex Rock Date: Wed, 9 Oct 2024 15:28:04 -0400 Subject: [PATCH 15/15] feat: 0.0.0 --- validate/string/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate/string/package.json b/validate/string/package.json index a0ab8c2d7..bc0cc3dda 100644 --- a/validate/string/package.json +++ b/validate/string/package.json @@ -1,6 +1,6 @@ { "name": "@flatfile/plugin-validate-string", - "version": "0.0.1", + "version": "0.0.0", "description": "A Flatfile plugin for string configuration and validation", "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/validate/string", "main": "./dist/index.cjs",