diff --git a/.changeset/witty-books-turn.md b/.changeset/witty-books-turn.md new file mode 100644 index 000000000..d9791c7d9 --- /dev/null +++ b/.changeset/witty-books-turn.md @@ -0,0 +1,5 @@ +--- +'@flatfile/plugin-stored-constraints': patch +--- + +Runs the stored constraints for an app diff --git a/package-lock.json b/package-lock.json index 376d11792..edc4cd06f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3837,7 +3837,9 @@ } }, "node_modules/@flatfile/api": { - "version": "1.10.0", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@flatfile/api/-/api-1.11.0.tgz", + "integrity": "sha512-kz7BdxdiCWEXZSSxwcB8BfujWUKAwNivlKuWM5UCyCAyp5qf4VEDfUgd3FQcxa6KJ01E6aYkkp+45ICxrJL/Cw==", "dependencies": { "@flatfile/cross-env-config": "0.0.4", "@types/pako": "2.0.1", @@ -4104,6 +4106,10 @@ "resolved": "plugins/space-configure", "link": true }, + "node_modules/@flatfile/plugin-stored-constraints": { + "resolved": "plugins/stored-constraints", + "link": true + }, "node_modules/@flatfile/plugin-tsv-extractor": { "resolved": "plugins/tsv-extractor", "link": true @@ -6136,9 +6142,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", - "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz", + "integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==", "cpu": [ "x64" ], @@ -8026,6 +8032,10 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/country-state-city": { + "version": "3.2.1", + "license": "GPL-3.0" + }, "node_modules/cross-fetch": { "version": "4.0.0", "license": "MIT", @@ -12347,6 +12357,13 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.5.0", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.12", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", @@ -15691,9 +15708,9 @@ } }, "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" }, "node_modules/stoppable": { "version": "1.1.0", @@ -16819,6 +16836,13 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -17635,10 +17659,10 @@ }, "plugins/json-extractor": { "name": "@flatfile/plugin-json-extractor", - "version": "0.10.0", + "version": "0.11.0", "license": "ISC", "dependencies": { - "@flatfile/util-extractor": "^2.3.0" + "@flatfile/util-extractor": "^2.4.0" }, "devDependencies": { "@flatfile/bundler-config-tsup": "^0.2.0", @@ -17864,6 +17888,29 @@ "@flatfile/listener": "^1.1.0" } }, + "plugins/stored-constraints": { + "name": "@flatfile/plugin-stored-constraints", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "country-state-city": "^3.2.1", + "luxon": "^3.5.0", + "validator": "^13.12.0" + }, + "devDependencies": { + "@flatfile/bundler-config-tsup": "^0.2.0", + "@flatfile/config-vitest": "^0.0.0", + "@flatfile/plugin-record-hook": "^1.10.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@flatfile/api": "^1.11.0", + "@flatfile/listener": "^1.1.0", + "@flatfile/plugin-record-hook": "^1.10.0" + } + }, "plugins/tsv-extractor": { "name": "@flatfile/plugin-tsv-extractor", "version": "1.10.0", @@ -18056,6 +18103,29 @@ "node": ">= 18" } }, + "utils/asdf": { + "name": "@flatfile/util-asdf", + "version": "1.0.0", + "extraneous": true, + "license": "ISC", + "dependencies": { + "chrono-node": "^2.7.7", + "collect.js": "^4.36.1", + "cross-fetch": "^4.0.0", + "jsonlines": "^0.1.1" + }, + "devDependencies": { + "@flatfile/bundler-config-tsup": "^0.2.0", + "@flatfile/config-vitest": "^0.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@flatfile/api": "^1.9.19", + "@flatfile/listener": "^1.1.0" + } + }, "utils/common": { "name": "@flatfile/util-common", "version": "1.6.0", @@ -18082,7 +18152,7 @@ }, "utils/extractor": { "name": "@flatfile/util-extractor", - "version": "2.3.0", + "version": "2.4.0", "license": "ISC", "dependencies": { "@flatfile/util-common": "^1.6.0", diff --git a/plugins/stored-constraints/CHANGELOG.md b/plugins/stored-constraints/CHANGELOG.md new file mode 100644 index 000000000..65fe599a5 --- /dev/null +++ b/plugins/stored-constraints/CHANGELOG.md @@ -0,0 +1 @@ +# @flatfile/plugin-stored-constraints \ No newline at end of file diff --git a/plugins/stored-constraints/README.md b/plugins/stored-constraints/README.md new file mode 100644 index 000000000..a4203cdd2 --- /dev/null +++ b/plugins/stored-constraints/README.md @@ -0,0 +1,27 @@ + + +# @flatfile/plugin-stored-constraints + +The `@flatfile/plugin-stored-constraints` plugin enables running stored constraints. + +**Event Type:** +`listener.on('commit:created')` + + + +## Installation + +```bash install +npm i @flatfile/plugin-stored-constraints +``` + +## Usage + +```ts listener.ts +import type { FlatfileListener } from "@flatfile/listener"; +import { storedConstraint } from '@flatfile/plugin-stored-constraints' + +export default function (listener: FlatfileListener) { + listener.use(storedConstraint()) +} +``` diff --git a/plugins/stored-constraints/package.json b/plugins/stored-constraints/package.json new file mode 100644 index 000000000..9cc003d70 --- /dev/null +++ b/plugins/stored-constraints/package.json @@ -0,0 +1,74 @@ +{ + "name": "@flatfile/plugin-stored-constraints", + "version": "0.0.0", + "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/plugins/stored-constraints", + "description": "A plugin for running stored constraints", + "type": "module", + "engines": { + "node": ">= 18" + }, + "registryMetadata": { + "category": "records" + }, + "exports": { + ".": { + "node": { + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts" + }, + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "browser": { + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts" + }, + "import": "./dist/index.browser.js", + "require": "./dist/index.browser.cjs" + }, + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "build:prod": "NODE_ENV=production tsup", + "checks": "tsc --noEmit && attw --pack . && publint .", + "lint": "tsc --noEmit", + "test": "vitest run --mode defaults src/*.spec.ts --passWithNoTests", + "test:unit": "vitest run --mode defaults src/*.spec.ts --passWithNoTests --exclude src/*.e2e.spec.ts", + "test:e2e": "vitest run --mode defaults src/*.e2e.spec.ts --passWithNoTests --no-file-parallelism" + }, + "keywords": [], + "repository": { + "type": "git", + "url": "git+https://github.com/FlatFilers/flatfile-plugins.git", + "directory": "plugins/stored-constraints" + }, + "license": "ISC", + "dependencies": { + "country-state-city": "^3.2.1", + "luxon": "^3.5.0", + "validator": "^13.12.0" + }, + "peerDependencies": { + "@flatfile/api": "^1.11.0", + "@flatfile/listener": "^1.1.0", + "@flatfile/plugin-record-hook": "^1.10.0" + }, + "devDependencies": { + "@flatfile/plugin-record-hook": "^1.10.0", + "@flatfile/bundler-config-tsup": "^0.2.0", + "@flatfile/config-vitest": "^0.0.0" + } +} diff --git a/plugins/stored-constraints/src/index.ts b/plugins/stored-constraints/src/index.ts new file mode 100644 index 000000000..4fd8ff18e --- /dev/null +++ b/plugins/stored-constraints/src/index.ts @@ -0,0 +1 @@ +export * from './stored.constraint' diff --git a/plugins/stored-constraints/src/stored.constraint.ts b/plugins/stored-constraints/src/stored.constraint.ts new file mode 100644 index 000000000..429b3eb43 --- /dev/null +++ b/plugins/stored-constraints/src/stored.constraint.ts @@ -0,0 +1,61 @@ +import type { Flatfile } from '@flatfile/api' +import type { FlatfileRecord } from '@flatfile/hooks' +import type { FlatfileEvent } from '@flatfile/listener' +import { bulkRecordHook } from '@flatfile/plugin-record-hook' +import * as countryStateCity from 'country-state-city' +import { DateTime } from 'luxon' +import validator from 'validator' +import { + applyConstraintToRecord, + crossEach, + getAppConstraints, + getFields, + getSheet, + getStoredConstraints, + getValidator, + hasStoredConstraints, +} from './utils' + +const deps = { validator, countryStateCity, luxon: DateTime } + +export interface Constraint { + validator: string + function: string + type?: string +} + +async function getValidators(event: FlatfileEvent): Promise { + const constraints = await getAppConstraints(event.context.appId) + return constraints.data.map((c: Flatfile.ConstraintResource) => { + return { + validator: c.validator, + function: c.function, + } + }) +} + +export function storedConstraint() { + return bulkRecordHook( + '**', + async (records: FlatfileRecord[], event: FlatfileEvent) => { + const sheet: Flatfile.SheetResponse = await getSheet(event) + const storedConstraintFields = + getFields(sheet).filter(hasStoredConstraints) + const validators = await getValidators(event) + + crossEach( + [records, storedConstraintFields], + (record: FlatfileRecord, field: Flatfile.Property) => { + getStoredConstraints(field.constraints).forEach( + async ({ validator }: { validator: string }) => { + const constraint = await getValidator(validators, validator) + if (constraint) { + applyConstraintToRecord(constraint, record, field, deps, sheet) + } + } + ) + } + ) + } + ) +} diff --git a/plugins/stored-constraints/src/utils.ts b/plugins/stored-constraints/src/utils.ts new file mode 100644 index 000000000..90ea5c325 --- /dev/null +++ b/plugins/stored-constraints/src/utils.ts @@ -0,0 +1,64 @@ +import { type Flatfile, FlatfileClient } from '@flatfile/api' +import type { FlatfileEvent } from '@flatfile/listener' +import type { FlatfileRecord } from '@flatfile/plugin-record-hook' +import { Constraint } from './stored.constraint' + +const api = new FlatfileClient() + +export const getSheet = async ( + event: FlatfileEvent +): Promise => + await api.sheets.get(event.context.sheetId) + +export const getFields = ({ data }: { data: Flatfile.Sheet }) => + data.config.fields + +export const getStoredConstraints = ( + constraints: Flatfile.Constraint[] +): Flatfile.StoredConstraint[] => + constraints?.filter((c) => c.type === 'stored') + +export const hasStoredConstraints = (field: Flatfile.Property) => + getStoredConstraints(field.constraints || []).length > 0 + +export const getValidator = ( + v: Constraint[], + n: string +): Constraint | undefined => v.find((w) => w.validator === n) + +export const applyConstraintToRecord = ( + constraint: Constraint, + record: FlatfileRecord, + field: Flatfile.Property, + deps: any, + sheet: Flatfile.SheetResponse +) => { + const storedConstraint = getStoredConstraints(field.constraints || []).find( + (fieldConstraint) => fieldConstraint.validator === constraint.validator + ) + const { config = {} } = storedConstraint || {} + const constraintFn = constraint.function.startsWith('function') + ? constraint.function.includes('function constraint') + ? eval( + '(' + + constraint.function.replace('function constraint', 'function') + + ')' + ) + : eval('(' + constraint.function + ')') + : eval(constraint.function) + + constraintFn(record.get(field.key), field.key, { + config, + record, + deps, + sheet, + }) +} +export const crossProduct = (...a: T[][]): T[][] => + a.reduce((u, c) => u.flatMap((x) => c.map((b) => [...x, b])), [[]]) +export const crossEach = (a: T[][], cb: (...args: T[]) => void): void => + crossProduct(...a).forEach((p) => cb(...p)) +export const getAppConstraints = async ( + a: Flatfile.AppId +): Promise => + await api.apps.getConstraints(a, { includeBuiltins: true }) diff --git a/plugins/stored-constraints/tsup.config.mjs b/plugins/stored-constraints/tsup.config.mjs new file mode 100644 index 000000000..bd6420831 --- /dev/null +++ b/plugins/stored-constraints/tsup.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from '@flatfile/bundler-config-tsup' + +export default defineConfig({}) diff --git a/plugins/stored-constraints/vitest.config.ts b/plugins/stored-constraints/vitest.config.ts new file mode 100644 index 000000000..e9e1729f9 --- /dev/null +++ b/plugins/stored-constraints/vitest.config.ts @@ -0,0 +1,4 @@ +import config from '@flatfile/config-vitest' +import { defineConfig } from 'vitest/config' + +export default defineConfig(config)