diff --git a/components/airtable_oauth/actions/common/common.mjs b/components/airtable_oauth/actions/common/common.mjs index 706036784002f..d1feef0608286 100644 --- a/components/airtable_oauth/actions/common/common.mjs +++ b/components/airtable_oauth/actions/common/common.mjs @@ -10,6 +10,11 @@ export default { ], withLabel: true, }, + warningAlert: { + type: "alert", + alertType: "warning", + content: "**Note:** if using a custom expression to specify the `Base` (e.g. `{{steps.mydata.$return_value}}`) you should also use a custom expression for the `Table`, and any other props that depend on it.", + }, tableId: { propDefinition: [ airtable, diff --git a/components/airtable_oauth/actions/create-comment/create-comment.mjs b/components/airtable_oauth/actions/create-comment/create-comment.mjs index 0654cf4064b8a..63e8bc27c0c7e 100644 --- a/components/airtable_oauth/actions/create-comment/create-comment.mjs +++ b/components/airtable_oauth/actions/create-comment/create-comment.mjs @@ -3,8 +3,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-create-comment", name: "Create Comment", - description: "Create a new comment on a record. [See the documentation](https://airtable.com/developers/web/api/create-comment)", - version: "0.0.7", + description: "Create a comment on a selected record. [See the documentation](https://airtable.com/developers/web/api/create-comment)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -23,7 +23,7 @@ export default { comment: { type: "string", label: "Comment", - description: "The text comment", + description: "The text comment to create", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/create-field/create-field.mjs b/components/airtable_oauth/actions/create-field/create-field.mjs index b141e4d8a39c7..870e87ff4f5ee 100644 --- a/components/airtable_oauth/actions/create-field/create-field.mjs +++ b/components/airtable_oauth/actions/create-field/create-field.mjs @@ -1,30 +1,53 @@ +import constants from "../../sources/common/constants.mjs"; import common from "../common/common.mjs"; export default { key: "airtable_oauth-create-field", name: "Create Field", description: "Create a new field in a table. [See the documentation](https://airtable.com/developers/web/api/create-field)", - version: "0.0.7", + version: "0.1.0", type: "action", props: { ...common.props, - field: { + name: { type: "string", - label: "Field", - description: "A JSON object representing the field. Refer to [field types](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", + label: "Field Name", + description: "The name of the field", + }, + type: { + type: "string", + label: "Field Type", + description: "The field type. [See the documentation](https://airtable.com/developers/web/api/model/field-type) for more information.", + options: constants.FIELD_TYPES, + }, + description: { + type: "string", + label: "Field Description", + description: "The description of the field", + optional: true, + }, + options: { + type: "object", + label: "Field Options", + description: "The options for the field as a JSON object, e.g. `{ \"color\": \"greenBright\" }`. Each type has a specific set of options - [see the documentation](https://airtable.com/developers/web/api/field-model) for more information.", + optional: true, }, }, async run({ $ }) { - const field = typeof this.field === "object" - ? this.field - : JSON.parse(this.field); - const data = { - ...field, - }; + const { + description, name, options, type, + } = this; const response = await this.airtable.createField({ baseId: this.baseId.value, tableId: this.tableId.value, - data, + data: { + name, + type, + description, + options: typeof options === "string" + ? JSON.parse(options) + : options, + }, $, }); diff --git a/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs b/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs index 44e4ed8a66fe6..ae42ef3634a6e 100644 --- a/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs +++ b/components/airtable_oauth/actions/create-multiple-records/create-multiple-records.mjs @@ -1,14 +1,15 @@ import chunk from "lodash.chunk"; import airtable from "../../airtable_oauth.app.mjs"; import common from "../common/common.mjs"; +import { ConfigurationError } from "@pipedream/platform"; const BATCH_SIZE = 10; // The Airtable API allows us to update up to 10 rows per request. export default { key: "airtable_oauth-create-multiple-records", name: "Create Multiple Records", - description: "Create one or more records in a table by passing an array of objects containing field names and values as key/value pairs. [See the documentation](https://airtable.com/developers/web/api/create-records)", - version: "0.0.7", + description: "Create one or more records in a table in a single operation with an array. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -18,6 +19,15 @@ export default { "records", ], }, + customExpressionInfo: { + type: "alert", + alertType: "info", + content: `You can use a custom expression that evaluates to an object for each entry in the array, e.g. \`{{ { "foo": "bar", "id": 123 } }}\`. +\\ +You can also reference an object exported by a previous step, e.g. \`{{steps.foo.$return_value}}\`. +\\ +If desired, you can use a custom expression in the same fashion for the entire array instead of providing individual values.`, + }, typecast: { propDefinition: [ airtable, @@ -39,9 +49,18 @@ export default { if (!Array.isArray(data)) { data = JSON.parse(data); } - data = data.map((fields) => ({ - fields, - })); + data = data.map((fields, index) => { + if (typeof fields === "string") { + try { + fields = JSON.parse(fields); + } catch (err) { + throw new ConfigurationError(`Error parsing record (index ${index}) as JSON: ${err.message}`); + } + } + return { + fields, + }; + }); if (!data.length) { throw new Error("No Airtable record data passed to step. Please pass at least one record"); } diff --git a/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs b/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs index 0e9d302a3a5c5..fd6862653af4c 100644 --- a/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs +++ b/components/airtable_oauth/actions/create-or-update-record/create-or-update-record.mjs @@ -4,13 +4,12 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-create-or-update-record", - name: "Create Single Record Or Update", - description: "Updates a record if `recordId` is provided or adds a record to a table.", - version: "0.0.8", + name: "Create or Update Record", + description: "Create a new record or update an existing one. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.1.0", type: "action", props: { ...common.props, - // eslint-disable-next-line pipedream/props-label,pipedream/props-description tableId: { ...common.props.tableId, reloadProps: true, @@ -27,7 +26,7 @@ export default { }), ], optional: true, - description: "Enter a [record ID](https://support.airtable.com/hc/en-us/articles/360051564873-Record-ID) if you want to update an existing record. Leave blank to create a new record.", + description: "To update an existing record, select it from the list or provide its [Record ID](https://support.airtable.com/hc/en-us/articles/360051564873-Record-ID). If left blank, a new record will be created.", }, typecast: { propDefinition: [ diff --git a/components/airtable_oauth/actions/create-single-record/create-single-record.mjs b/components/airtable_oauth/actions/create-single-record/create-single-record.mjs index b711e06fee2ae..49466af77a122 100644 --- a/components/airtable_oauth/actions/create-single-record/create-single-record.mjs +++ b/components/airtable_oauth/actions/create-single-record/create-single-record.mjs @@ -6,7 +6,7 @@ export default { key: "airtable_oauth-create-single-record", name: "Create Single Record", description: "Adds a record to a table.", - version: "0.0.8", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/create-table/create-table.mjs b/components/airtable_oauth/actions/create-table/create-table.mjs index 04095c968bd38..c34f9ad644128 100644 --- a/components/airtable_oauth/actions/create-table/create-table.mjs +++ b/components/airtable_oauth/actions/create-table/create-table.mjs @@ -4,7 +4,7 @@ export default { key: "airtable_oauth-create-table", name: "Create Table", description: "Create a new table. [See the documentation](https://airtable.com/developers/web/api/create-table)", - version: "0.0.7", + version: "0.0.8", type: "action", props: { airtable, @@ -17,18 +17,18 @@ export default { name: { type: "string", label: "Name", - description: "The name for the table", + description: "The name of the table", }, description: { type: "string", label: "Description", - description: "The description for the table", + description: "The description of the table", optional: true, }, fields: { type: "string[]", label: "Fields", - description: "A list of JSON objects representing the fields in the table. Refer to [field types](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", + description: "A list of JSON objects representing the fields in the table. [See the documentation](https://airtable.com/developers/web/api/model/field-type) for supported field types, the write format for field options, and other specifics for certain field types.", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/delete-record/delete-record.mjs b/components/airtable_oauth/actions/delete-record/delete-record.mjs index 2b13c5b41d8b0..9748b28a0ab9a 100644 --- a/components/airtable_oauth/actions/delete-record/delete-record.mjs +++ b/components/airtable_oauth/actions/delete-record/delete-record.mjs @@ -4,8 +4,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-delete-record", name: "Delete Record", - description: "Delete a record from a table by record ID. [See the documentation](https://airtable.com/developers/web/api/delete-record)", - version: "0.0.7", + description: "Delete a selected record from a table. [See the documentation](https://airtable.com/developers/web/api/delete-record)", + version: "0.0.8", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs b/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs index 6c9d438592663..295986aefbe54 100644 --- a/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs +++ b/components/airtable_oauth/actions/get-record-or-create/get-record-or-create.mjs @@ -5,12 +5,11 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-get-record-or-create", name: "Get Record Or Create", - description: "Get a record from a table by record ID or create a new register.", - version: "0.0.8", + description: "Get a specific record, or create one if it doesn't exist. [See the documentation](https://airtable.com/developers/web/api/create-records)", + version: "0.0.9", type: "action", props: { ...common.props, - // eslint-disable-next-line pipedream/props-label,pipedream/props-description tableId: { ...common.props.tableId, reloadProps: true, diff --git a/components/airtable_oauth/actions/get-record/get-record.mjs b/components/airtable_oauth/actions/get-record/get-record.mjs index 697846e04d017..a579faec3b028 100644 --- a/components/airtable_oauth/actions/get-record/get-record.mjs +++ b/components/airtable_oauth/actions/get-record/get-record.mjs @@ -5,8 +5,8 @@ import commonActions from "../../common/actions.mjs"; export default { key: "airtable_oauth-get-record", name: "Get Record", - description: "Get a record from a table by record ID. [See the documentation](https://airtable.com/developers/web/api/get-record)", - version: "0.0.8", + description: "Get data of a selected record from a table. [See the documentation](https://airtable.com/developers/web/api/get-record)", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs b/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs index 4599642acc383..ae2eeea18e221 100644 --- a/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs +++ b/components/airtable_oauth/actions/list-records-in-view/list-records-in-view.mjs @@ -4,11 +4,16 @@ import commonList from "../common/common-list.mjs"; export default { key: "airtable_oauth-list-records-in-view", name: "List Records in View", - description: "Retrieve records in a view with automatic pagination. Optionally sort and filter results. Only available for Enterprise accounts.", + description: "Retrieve records from a view, optionally sorting and filtering results. [See the documentation](https://airtable.com/developers/web/api/list-views)", type: "action", - version: "0.0.7", + version: "0.0.8", ...commonList, props: { + accountTierAlert: { + type: "alert", + alertType: "info", + content: "Note: views are only available for Airtable Enterprise accounts. [See the documentation](https://airtable.com/developers/web/api/list-views) for more information.", + }, ...common.props, viewId: { propDefinition: [ diff --git a/components/airtable_oauth/actions/list-records/list-records.mjs b/components/airtable_oauth/actions/list-records/list-records.mjs index c58dce9021fe4..c8dcf06f0a013 100644 --- a/components/airtable_oauth/actions/list-records/list-records.mjs +++ b/components/airtable_oauth/actions/list-records/list-records.mjs @@ -4,9 +4,9 @@ import commonList from "../common/common-list.mjs"; export default { key: "airtable_oauth-list-records", name: "List Records", - description: "Retrieve records from a table with automatic pagination. Optionally sort and filter results.", + description: "Retrieve records from a table, optionally sorting and filtering results. [See the documentation](https://airtable.com/developers/web/api/list-records)", type: "action", - version: "0.0.7", + version: "0.0.8", ...commonList, props: { ...common.props, diff --git a/components/airtable_oauth/actions/search-records/search-records.mjs b/components/airtable_oauth/actions/search-records/search-records.mjs index a4cdb9a649a2c..3465137d21e1f 100644 --- a/components/airtable_oauth/actions/search-records/search-records.mjs +++ b/components/airtable_oauth/actions/search-records/search-records.mjs @@ -4,8 +4,8 @@ import { fieldTypeToPropType } from "../../common/utils.mjs"; export default { key: "airtable_oauth-search-records", name: "Search Records", - description: "Searches for a record by formula or by field value. [See the documentation](https://airtable.com/developers/web/api/list-records)", - version: "0.0.9", + description: "Search for a record by formula or by field value. [See the documentation](https://airtable.com/developers/web/api/list-records)", + version: "0.0.10", type: "action", props: { ...common.props, @@ -32,7 +32,7 @@ export default { props.searchFormula = { type: "string", label: "Search Formula", - description: "Use an Airtable search formula to find records. For example, if you want to find records with `Tags` includes `test-1`, use `FIND('test-1', {Tags})`. Learn more on [Airtable's website](https://support.airtable.com/docs/formula-field-reference)", + description: "Use an [Airtable search formula (see info on the documentation)](https://support.airtable.com/docs/formula-field-reference) to find records. For example, if you want to find records with `Tags` including `test-1`, use `FIND('test-1', {Tags})`.", optional: true, }; } diff --git a/components/airtable_oauth/actions/update-comment/update-comment.mjs b/components/airtable_oauth/actions/update-comment/update-comment.mjs index 2fb997cb19c8b..9ff68dc33def5 100644 --- a/components/airtable_oauth/actions/update-comment/update-comment.mjs +++ b/components/airtable_oauth/actions/update-comment/update-comment.mjs @@ -3,8 +3,8 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-update-comment", name: "Update Comment", - description: "Updates an existing comment on a record. [See the documentation](https://airtable.com/developers/web/api/update-comment)", - version: "0.0.7", + description: "Update an existing comment on a selected record. [See the documentation](https://airtable.com/developers/web/api/update-comment)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -36,7 +36,7 @@ export default { comment: { type: "string", label: "Comment", - description: "The text comment", + description: "The new content of the comment", }, }, async run({ $ }) { diff --git a/components/airtable_oauth/actions/update-field/update-field.mjs b/components/airtable_oauth/actions/update-field/update-field.mjs index cdf808501266b..6662c21391496 100644 --- a/components/airtable_oauth/actions/update-field/update-field.mjs +++ b/components/airtable_oauth/actions/update-field/update-field.mjs @@ -4,8 +4,8 @@ import { ConfigurationError } from "@pipedream/platform"; export default { key: "airtable_oauth-update-field", name: "Update Field", - description: "Updates an existing field in a table. [See the documentation](https://airtable.com/developers/web/api/update-field)", - version: "0.0.7", + description: "Update an existing field in a table. [See the documentation](https://airtable.com/developers/web/api/update-field)", + version: "0.0.8", type: "action", props: { ...common.props, @@ -27,19 +27,19 @@ export default { name: { type: "string", label: "Name", - description: "The name of the field", + description: "The new name of the field", optional: true, }, description: { type: "string", label: "Description", - description: "The description for the field", + description: "The new description of the field", optional: true, }, }, async run({ $ }) { if (!this.name && !this.description) { - throw new ConfigurationError("At least one of `name` or `description` must be provided."); + throw new ConfigurationError("At least one of `Name` or `Description` must be provided."); } const data = {}; diff --git a/components/airtable_oauth/actions/update-record/update-record.mjs b/components/airtable_oauth/actions/update-record/update-record.mjs index 5327c8282a703..9a8694e726185 100644 --- a/components/airtable_oauth/actions/update-record/update-record.mjs +++ b/components/airtable_oauth/actions/update-record/update-record.mjs @@ -6,7 +6,7 @@ export default { key: "airtable_oauth-update-record", name: "Update Record", description: "Update a single record in a table by Record ID. [See the documentation](https://airtable.com/developers/web/api/update-record)", - version: "0.0.8", + version: "0.0.9", type: "action", props: { ...common.props, diff --git a/components/airtable_oauth/actions/update-table/update-table.mjs b/components/airtable_oauth/actions/update-table/update-table.mjs index d9a4c5eb89ec3..44b8c9be1ce71 100644 --- a/components/airtable_oauth/actions/update-table/update-table.mjs +++ b/components/airtable_oauth/actions/update-table/update-table.mjs @@ -3,21 +3,21 @@ import common from "../common/common.mjs"; export default { key: "airtable_oauth-update-table", name: "Update Table", - description: "Updates an existing table. [See the documentation](https://airtable.com/developers/web/api/update-table)", - version: "0.0.7", + description: "Update an existing table. [See the documentation](https://airtable.com/developers/web/api/update-table)", + version: "0.0.8", type: "action", props: { ...common.props, name: { type: "string", label: "Name", - description: "The name for the table", + description: "The updated name of the table", optional: true, }, description: { type: "string", label: "Description", - description: "The description for the table", + description: "The updated description of the table", optional: true, }, }, diff --git a/components/airtable_oauth/airtable_oauth.app.mjs b/components/airtable_oauth/airtable_oauth.app.mjs index 0121f7915944a..5d588c6572b59 100644 --- a/components/airtable_oauth/airtable_oauth.app.mjs +++ b/components/airtable_oauth/airtable_oauth.app.mjs @@ -18,7 +18,7 @@ export default { baseId: { type: "string", label: "Base", - description: "The base ID", + description: "The Base ID.", async options({ prevContext }) { const params = {}; if (prevContext?.newOffset) { @@ -44,7 +44,7 @@ export default { tableId: { type: "string", label: "Table", - description: "The table ID. If referencing a **Base** dynamically using data from another step (e.g., `{{steps.trigger.event.metadata.baseId}}`), you will not be able to select from the list of Tables, and automatic table options will not work when configuring this step. Please enter a custom expression to specify the **Table**.", + description: "The Table ID.", async options({ baseId }) { let tables; try { @@ -63,7 +63,7 @@ export default { viewId: { type: "string", label: "View", - description: "The view ID. If referencing a **Table** dynamically using data from another step (e.g., `{{steps.trigger.event.metadata.tableId}}`), you will not be able to select from the list of Views for this step. Please enter a custom expression to specify the **View**.", + description: "The View ID.", async options({ baseId, tableId, }) { @@ -85,8 +85,8 @@ export default { }, sortFieldId: { type: "string", - label: "Sort: Field", - description: "Optionally select a field to sort results. To sort by multiple fields, use the **Filter by Forumla** field. If referencing a **Table** dynamically using data from another step (e.g., `{{steps.mydata.$return_value}}`), automatic field options won't work when configuring this step. Please enter a custom expression to specify the **Sort: Field**.", + label: "Sort by Field", + description: "Optionally select a field to sort results by. To sort by multiple fields, use the **Filter by Formula** field.", optional: true, async options({ baseId, tableId, @@ -180,14 +180,14 @@ export default { }, returnFieldsByFieldId: { type: "boolean", - label: "Return Fields By Field ID", - description: "An optional boolean value that lets you return field objects where the key is the field id. This defaults to `false`, which returns field objects where the key is the field name.", + label: "Return Fields By ID", + description: "If set to `true`, the returned field objects will have the field ID as the key, instead of the field name.", optional: true, }, sortDirection: { type: "string", label: "Sort: Direction", - description: "This field will be ignored if you don't select a field to sort by.", + description: "If sorting by a field, which direction to sort by.", options: SORT_DIRECTION_OPTIONS, default: "desc", optional: true, @@ -195,30 +195,37 @@ export default { maxRecords: { type: "integer", label: "Max Records", - description: "Optionally limit the maximum number of records to return. Leave blank to retrieve all records.", + description: "The maximum number of records to return. Leave blank to retrieve all records.", optional: true, }, filterByFormula: { type: "string", label: "Filter by Formula", - description: "Optionally provide a [formula](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) used to filter records. The formula will be evaluated for each record, and if the result is not `0`, `false`, `\"\"`, `NaN`, `[]`, or `#Error!` the record will be included in the response. For example, to only include records where `Name` isn't empty, pass `NOT({Name} = '')`.", + description: "Optionally provide a [formula (see info on the documentation)](https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference) used to filter records. The formula will be evaluated for each record, and if the result is not `0`, `false`, `\"\"`, `NaN`, `[]`, or `#Error!` the record will be included in the response. For example, to only include records where `Name` isn't empty, use `NOT({Name} = '')`.", optional: true, }, records: { - type: "string", + type: "string[]", label: "Records", - description: "Provide an array of objects. Each object should represent a new record with the column name as the key and the data to insert as the corresponding value (e.g., passing `[{\"foo\":\"bar\",\"id\":123},{\"foo\":\"baz\",\"id\":456}]` will create two records and with values added to the fields `foo` and `id`). The most common pattern is to reference an array of objects exported by a previous step (e.g., `{{steps.foo.$return_value}}`). You may also enter or construct a string that will `JSON.parse()` to an array of objects.", + description: "Each item in the array should be an object in JSON format, representing a new record. The keys are the column names and the corresponding values are the data to insert.", }, typecast: { type: "boolean", label: "Typecast", - description: "The Airtable API will perform best-effort automatic data conversion from string values if the typecast parameter is `True`. Automatic conversion is disabled by default to ensure data integrity, but it may be helpful for integrating with 3rd party data sources.", + description: "The Airtable API will perform best-effort automatic data conversion from string values if the typecast parameter is `True`. This is disabled by default to ensure data integrity, but it may be helpful for integrating with 3rd party data sources.", optional: true, }, record: { type: "object", label: "Record", - description: "Enter the column name for the key and the corresponding column value. You can include all, some, or none of the field values. You may also pass a JSON object as a custom expression with key/value pairs representing columns and values (e.g., `{{ {\"foo\":\"bar\",\"id\":123} }}`). A common pattern is to reference an object exported by a previous step (e.g., `{{steps.foo.$return_value}}`).", + description: "Enter the column name for the key and the corresponding column value. You can include all, some, or none of the field values. You may also use a custom expression.", + }, + customExpressionInfo: { + type: "alert", + alertType: "info", + content: `A custom expression can be a JSON object with key/value pairs representing columns and values, e.g. \`{{ { "foo": "bar", "id": 123 } }}\`. +\\ +You can also reference an object exported by a previous step, e.g. \`{{steps.foo.$return_value}}\`.`, }, }, methods: { diff --git a/components/airtable_oauth/common/actions.mjs b/components/airtable_oauth/common/actions.mjs index 4680b3adcab87..cac8ad905e6f6 100644 --- a/components/airtable_oauth/common/actions.mjs +++ b/components/airtable_oauth/common/actions.mjs @@ -23,6 +23,7 @@ export default { // Use record propDefinition directly to workaround lack of support // for propDefinition in additionalProps record: airtable.propDefinitions.record, + customExpressionInfo: airtable.propDefinitions.customExpressionInfo, }; } throw new ConfigurationError("Could not find a table for the specified base ID and table ID. Please adjust the action configuration to continue."); diff --git a/components/airtable_oauth/common/utils.mjs b/components/airtable_oauth/common/utils.mjs index 6ab4e6a8ea20b..cdf11cd5a576e 100644 --- a/components/airtable_oauth/common/utils.mjs +++ b/components/airtable_oauth/common/utils.mjs @@ -70,7 +70,7 @@ function fieldToProp(field) { return { type: fieldTypeToPropType(field.type), label: field.name, - description: field.description, + description: field.description ?? `Field type: \`${field.type}\`. Field ID: \`${field.id}\``, optional: true, options: field.options?.choices?.map((choice) => ({ label: choice.name || choice.id, diff --git a/components/airtable_oauth/package.json b/components/airtable_oauth/package.json index c148bd6ac266c..fe533ec5db12b 100644 --- a/components/airtable_oauth/package.json +++ b/components/airtable_oauth/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/airtable_oauth", - "version": "0.3.1", + "version": "0.4.0", "description": "Pipedream Airtable (OAuth) Components", "main": "airtable_oauth.app.mjs", "keywords": [ @@ -16,7 +16,6 @@ "@pipedream/platform": "^1.6.0", "airtable": "^0.11.1", "bottleneck": "^2.19.5", - "lodash": "^4.17.21", "lodash.chunk": "^4.2.0", "lodash.isempty": "^4.4.0", "moment": "^2.30.1" diff --git a/components/airtable_oauth/sources/common/common-webhook-field.mjs b/components/airtable_oauth/sources/common/common-webhook-field.mjs new file mode 100644 index 0000000000000..90b206ab963eb --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook-field.mjs @@ -0,0 +1,74 @@ +import common from "./common-webhook.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + getDataTypes() { + return [ + "tableFields", + ]; + }, + async saveAdditionalData() { + const tableData = await this.airtable.listTables({ + baseId: this.baseId, + }); + const filteredData = tableData?.tables?.map(({ + id, name, fields, + }) => ({ + id, + name, + fields, + })); + if (filteredData?.length) { + this.db.set("tableSchemas", filteredData); + } + }, + async emitEvent(payload) { + const [ + tableId, + tableData, + ] = Object.entries(payload.changedTablesById)[0]; + const [ + operation, + fieldObj, + ] = Object.entries(tableData)[0]; + const [ + fieldId, + fieldUpdateInfo, + ] = Object.entries(fieldObj)[0]; + + const timestamp = Date.parse(payload.timestamp); + if (this.isDuplicateEvent(fieldId, timestamp)) return; + this._setLastObjectId(fieldId); + this._setLastTimestamp(timestamp); + + const updateType = operation === "createdFieldsById" + ? "created" + : "updated"; + + let table = { + id: tableId, + }; + let field = { + id: fieldId, + }; + + const tableSchemas = this.db.get("tableSchemas"); + if (tableSchemas) { + table = tableSchemas.find(({ id }) => id === tableId); + field = table?.fields.find(({ id }) => id === fieldId); + delete table.fields; + } + + const summary = `Field ${updateType}: ${field.name ?? fieldUpdateInfo?.name ?? field.id}`; + + this.$emit({ + originalPayload: payload, + table, + field, + fieldUpdateInfo, + }, this.generateMeta(payload, summary)); + }, + }, +}; diff --git a/components/airtable_oauth/sources/common/common-webhook-record.mjs b/components/airtable_oauth/sources/common/common-webhook-record.mjs new file mode 100644 index 0000000000000..a610d402e830a --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook-record.mjs @@ -0,0 +1,74 @@ +import common from "./common-webhook.mjs"; + +export default { + ...common, + methods: { + ...common.methods, + getDataTypes() { + return [ + "tableData", + ]; + }, + async emitEvent(payload) { + const [ + tableId, + tableData, + ] = Object.entries(payload.changedTablesById)[0]; + let [ + operation, + recordObj, + ] = Object.entries(tableData)[0]; + if (operation === "changedViewsById") { + const changedRecord = Object.entries(recordObj)[0]; + operation = changedRecord[0]; + recordObj = changedRecord[1]; + } + + // for deleted record(s) we'll emit only their ids (no other info is available) + if (operation === "destroyedRecordIds" && Array.isArray(recordObj)) { + const { length } = recordObj; + const summary = length === 1 + ? `Record deleted: ${recordObj[0]}` + : `${length} records deleted`; + this.$emit({ + originalPayload: payload, + tableId, + deletedRecordIds: recordObj, + }, this.generateMeta(payload, summary)); + return; + } + + const [ + recordId, + recordUpdateInfo, + ] = Object.entries(recordObj)[0]; + + const timestamp = Date.parse(payload.timestamp); + if (this.isDuplicateEvent(recordId, timestamp)) return; + this._setLastObjectId(recordId); + this._setLastTimestamp(timestamp); + + let updateType = operation === "createdRecordsById" + ? "created" + : "updated"; + + const { fields } = await this.airtable.getRecord({ + baseId: this.baseId, + tableId, + recordId, + }); + + const summary = `Record ${updateType}: ${fields?.name ?? recordId}`; + + this.$emit({ + originalPayload: payload, + tableId, + record: { + id: recordId, + fields, + }, + recordUpdateInfo, + }, this.generateMeta(payload, summary)); + }, + }, +}; diff --git a/components/airtable_oauth/sources/common/common-webhook.mjs b/components/airtable_oauth/sources/common/common-webhook.mjs new file mode 100644 index 0000000000000..8375fcc85fa22 --- /dev/null +++ b/components/airtable_oauth/sources/common/common-webhook.mjs @@ -0,0 +1,186 @@ +import airtable from "../../airtable_oauth.app.mjs"; +import constants from "../common/constants.mjs"; + +export default { + props: { + airtable, + db: "$.service.db", + http: { + type: "$.interface.http", + customResponse: true, + }, + baseId: { + propDefinition: [ + airtable, + "baseId", + ], + }, + tableId: { + propDefinition: [ + airtable, + "tableId", + (c) => ({ + baseId: c.baseId, + }), + ], + description: "If specified, events will only be emitted for the selected Table", + optional: true, + }, + viewId: { + propDefinition: [ + airtable, + "viewId", + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, + }), + ], + description: "If specified, events will only be emitted for the selected View", + optional: true, + }, + fromSources: { + type: "string[]", + label: "From Sources", + description: "Only emit events for updates from these sources. If omitted, updates from all sources are reported", + options: constants.FROM_SOURCES, + optional: true, + }, + }, + hooks: { + async activate() { + const { id } = await this.airtable.createWebhook({ + baseId: this.baseId, + data: { + notificationUrl: `${this.http.endpoint}/`, + specification: { + options: { + filters: { + recordChangeScope: this.viewId + ? this.viewId + : this.tableId + ? this.tableId + : undefined, + dataTypes: this.getDataTypes(), + changeTypes: this.getChangeTypes(), + fromSources: this.fromSources, + watchDataInFieldIds: this.watchDataInFieldIds, + watchSchemasOfFieldIds: this.watchSchemasOfFieldIds, + }, + includes: { + includeCellValuesInFieldIds: this.includeCellValuesInFieldIds, + includePreviousCellValues: true, + includePreviousFieldDefinitions: true, + }, + }, + }, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.airtable.deleteWebhook({ + baseId: this.baseId, + webhookId, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getLastObjectId() { + return this.db.get("lastObjectId"); + }, + async _setLastObjectId(id) { + this.db.set("lastObjectId", id); + }, + _getLastTimestamp() { + return this.db.get("lastTimestamp"); + }, + async _setLastTimestamp(ts) { + this.db.set("lastTimestamp", ts); + }, + isDuplicateEvent(id, ts) { + const lastId = this._getLastObjectId(); + const lastTs = this._getLastTimestamp(); + + if (id === lastId && (ts - lastTs < 5000 )) { + console.log("Skipping trigger: another event was emitted for the same object within the last 5 seconds"); + return true; + } + + return false; + }, + getSpecificationOptions() { + throw new Error("getSpecificationOptions is not implemented"); + }, + generateMeta(payload, summary) { + return { + id: payload.baseTransactionNumber, + summary: summary ?? `New Event ${payload.baseTransactionNumber}`, + ts: Date.parse(payload.timestamp), + }; + }, + getDataTypes() { + return this.dataTypes; + }, + getChangeTypes() { + return this.changeTypes; + }, + emitDefaultEvent(payload) { + const meta = this.generateMeta(payload); + this.$emit(payload, meta); + }, + async emitEvent(payload) { + // sources may call this to customize event emission, but it is + // a separate method so that the source may fall back to default + return this.emitDefaultEvent(payload); + }, + async saveAdditionalData() { + // sources may call this to fetch additional data (e.g. field schemas) + // and it can be silently ignored when not required + return true; + }, + }, + async run() { + this.http.respond({ + status: 200, + }); + // webhook pings source, we then fetch webhook events to emit + const webhookId = this._getHookId(); + let hasMore = false; + const params = {}; + try { + await this.saveAdditionalData(); + } catch (err) { + console.log("Error fetching additional data, proceeding to event emission"); + console.log(err); + } + do { + const { + cursor, mightHaveMore, payloads, + } = await this.airtable.listWebhookPayloads({ + baseId: this.baseId, + webhookId, + params, + }); + for (const payload of payloads) { + try { + await this.emitEvent(payload); + } catch (err) { + console.log("Error emitting event, defaulting to default emission"); + console.log(err); + this.emitDefaultEvent(payload); + } + } + params.cursor = cursor; + hasMore = mightHaveMore; + } while (hasMore); + }, +}; diff --git a/components/airtable_oauth/sources/common/constants.mjs b/components/airtable_oauth/sources/common/constants.mjs index 230e6497e8d96..dcabb1533a205 100644 --- a/components/airtable_oauth/sources/common/constants.mjs +++ b/components/airtable_oauth/sources/common/constants.mjs @@ -14,9 +14,18 @@ const DATA_TYPES = [ ]; const CHANGE_TYPES = [ - "add", - "remove", - "update", + { + label: "Created", + value: "add", + }, + { + label: "Deleted", + value: "remove", + }, + { + label: "Updated/Modified", + value: "update", + }, ]; const FROM_SOURCES = [ @@ -34,7 +43,8 @@ const FROM_SOURCES = [ }, { value: "formPageSubmission", - label: "Changes generated when an interface form builder page, form layout page, or record creation button page is submitted", + label: + "Changes generated when an interface form builder page, form layout page, or record creation button page is submitted", }, { value: "automation", @@ -42,7 +52,8 @@ const FROM_SOURCES = [ }, { value: "system", - label: "Changes generated by system events, such as processing time function formulas", + label: + "Changes generated by system events, such as processing time function formulas", }, { value: "sync", @@ -58,8 +69,46 @@ const FROM_SOURCES = [ }, ]; +const FIELD_TYPES = [ + "singleLineText", + "email", + "url", + "multilineText", + "number", + "percent", + "currency", + "singleSelect", + "multipleSelects", + "singleCollaborator", + "multipleCollaborators", + "multipleRecordLinks", + "date", + "dateTime", + "phoneNumber", + "multipleAttachments", + "checkbox", + "formula", + "createdTime", + "rollup", + "count", + "lookup", + "multipleLookupValues", + "autoNumber", + "barcode", + "rating", + "richText", + "duration", + "lastModifiedTime", + "button", + "createdBy", + "lastModifiedBy", + "externalSyncSource", + "aiText", +]; + export default { DATA_TYPES, CHANGE_TYPES, FROM_SOURCES, + FIELD_TYPES, }; diff --git a/components/airtable_oauth/sources/new-field/new-field.mjs b/components/airtable_oauth/sources/new-field/new-field.mjs index 6db13d3371942..e23469e6b0088 100644 --- a/components/airtable_oauth/sources/new-field/new-field.mjs +++ b/components/airtable_oauth/sources/new-field/new-field.mjs @@ -1,52 +1,19 @@ -import common from "../common/common.mjs"; +import common from "../common/common-webhook-field.mjs"; export default { - name: "New Field", - description: "Emit new event for each new field created in a table", + ...common, + name: "New Field Created (Instant)", + description: "Emit new event when a field is created in the selected table. [See the documentation](https://airtable.com/developers/web/api/get-base-schema)", key: "airtable_oauth-new-field", - version: "0.0.7", + version: "1.0.0", type: "source", - props: { - ...common.props, - tableId: { - propDefinition: [ - common.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", - }, - }, + dedupe: "unique", methods: { - _getFieldIds() { - return this.db.get("fieldIds") || []; + ...common.methods, + getChangeTypes() { + return [ + "add", + ]; }, - _setFieldIds(fieldIds) { - this.db.set("fieldIds", fieldIds); - }, - }, - async run() { - const fieldIds = this._getFieldIds(); - - const { tables } = await this.airtable.listTables({ - baseId: this.baseId, - }); - const { fields } = tables.find(({ id }) => id === this.tableId); - - for (const field of fields) { - if (fieldIds.includes(field.id)) { - continue; - } - fieldIds.push(field.id); - this.$emit(field, { - id: field.id, - summary: field.name, - ts: Date.now(), - }); - } - - this._setFieldIds(fieldIds); }, }; diff --git a/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs index 232241063889b..ed93e57ea63e9 100644 --- a/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs +++ b/components/airtable_oauth/sources/new-modified-or-deleted-records-instant/new-modified-or-deleted-records-instant.mjs @@ -1,69 +1,29 @@ -import airtable from "../../airtable_oauth.app.mjs"; +import common from "../common/common-webhook-record.mjs"; import constants from "../common/constants.mjs"; import sampleEmit from "./test-event.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; export default { - name: "New Modified or Deleted Records (Instant)", - description: "Emit new event each time a record is added, updated, or deleted in an Airtable table. [See the documentation](https://airtable.com/developers/web/api/create-a-webhook)", + ...common, + name: "New Record Created, Updated or Deleted (Instant)", + description: "Emit new event when a record is added, updated, or deleted in a table or selected view.", key: "airtable_oauth-new-modified-or-deleted-records-instant", - version: "0.0.2", + version: "0.1.0", type: "source", dedupe: "unique", props: { - airtable, - db: "$.service.db", - http: { - type: "$.interface.http", - customResponse: true, - }, - baseId: { - propDefinition: [ - airtable, - "baseId", - ], - }, - dataTypes: { - type: "string[]", - label: "Data Types", - description: "Only generate payloads that contain changes affecting objects of these types", - options: constants.DATA_TYPES, - }, - tableId: { - propDefinition: [ - airtable, - "tableId", - (c) => ({ - baseId: c.baseId, - }), - ], - description: "Only generate payloads for changes in the specified TableId", - optional: true, - }, - viewId: { - propDefinition: [ - airtable, - "viewId", - (c) => ({ - baseId: c.baseId, - tableId: c.tableId, - }), - ], - description: "Only generate payloads for changes in the specified ViewId", - optional: true, - }, + ...common.props, changeTypes: { type: "string[]", - label: "Change Types", - description: "Only generate payloads that contain changes of these types", + label: "Update Types", + description: "Select the types of record updates that should emit events. If not specified, all updates will emit events.", options: constants.CHANGE_TYPES, optional: true, - }, - fromSouces: { - type: "string[]", - label: "From Sources", - description: "Only generate payloads for changes from these sources. If omitted, changes from all sources are reported", - options: constants.FROM_SOURCES, - optional: true, + default: [ + "add", + "remove", + "update", + ], }, watchDataInFieldIds: { propDefinition: [ @@ -76,129 +36,17 @@ export default { ], type: "string[]", label: "Watch Data In Field Ids", - description: "Only generate payloads for changes that modify values in cells in these fields. If omitted, all fields within the table/view/base are watched", - }, - watchSchemasOfFieldIds: { - propDefinition: [ - airtable, - "sortFieldId", - (c) => ({ - baseId: c.baseId, - tableId: c.tableId, - }), - ], - type: "string[]", - label: "Watch Schemas of Field Ids", - description: "Only generate payloads for changes that modify the schemas of these fields. If omitted, schemas of all fields within the table/view/base are watched", - }, - includeCellValuesInFieldIds: { - propDefinition: [ - airtable, - "sortFieldId", - (c) => ({ - baseId: c.baseId, - tableId: c.tableId, - }), - ], - type: "string[]", - label: "Include Cell Values in Field Ids", - description: "A list of fields to include in the payload regardless of whether or not they changed", - }, - includePreviousCellValues: { - type: "boolean", - label: "Include Previous Cell Values", - description: "If true, include the previous cell value in the payload", - optional: true, - }, - includePreviousFieldDefinitions: { - type: "boolean", - label: "Include Previous Field Definitions", - description: "If true, include the previous field definition in the payload", - optional: true, - }, - }, - hooks: { - async activate() { - const { id } = await this.airtable.createWebhook({ - baseId: this.baseId, - data: { - notificationUrl: `${this.http.endpoint}/`, - specification: { - options: { - filters: { - recordChangeScope: this.viewId - ? this.viewId - : this.tableId - ? this.tableId - : undefined, - dataTypes: this.dataTypes, - changeTypes: this.changeTypes, - fromSources: this.fromSources, - watchDataInFieldIds: this.watchDataInFieldIds, - watchSchemasOfFieldIds: this.watchSchemasOfFieldIds, - }, - includes: { - includeCellValuesInFieldIds: this.includeCellValuesInFieldIds, - includePreviousCellValues: this.includePreviousCellValues, - includePreviousFieldDefinitions: this.includePreviousFieldDefinitions, - }, - }, - }, - }, - }); - this._setHookId(id); - }, - async deactivate() { - const webhookId = this._getHookId(); - if (webhookId) { - await this.airtable.deleteWebhook({ - baseId: this.baseId, - webhookId, - }); - } + description: + "Only emit events for updates that modify values in cells in these fields. If omitted, all fields within the table/view/base are watched", }, }, methods: { - _getHookId() { - return this.db.get("hookId"); - }, - _setHookId(hookId) { - this.db.set("hookId", hookId); + ...common.methods, + getDataTypes() { + return [ + "tableData", + ]; }, - getSpecificationOptions() { - throw new Error("getSpecificationOptions is not implemented"); - }, - generateMeta(payload) { - return { - id: payload.baseTransactionNumber, - summary: `New Event ${payload.baseTransactionNumber}`, - ts: Date.parse(payload.timestamp), - }; - }, - }, - async run() { - this.http.respond({ - status: 200, - }); - // webhook pings source, we then fetch webhook events to emit - const webhookId = this._getHookId(); - let hasMore = false; - const params = {}; - do { - const { - cursor, mightHaveMore, payloads, - } = await this.airtable.listWebhookPayloads({ - baseId: this.baseId, - webhookId, - params, - }); - for (const payload of payloads) { - const meta = this.generateMeta(payload); - this.$emit(payload, meta); - } - params.cursor = cursor; - hasMore = mightHaveMore; - } while (hasMore); }, sampleEmit, }; diff --git a/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs b/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs index ed37e8d2a2beb..a288733b85fc5 100644 --- a/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs +++ b/components/airtable_oauth/sources/new-modified-or-deleted-records/new-modified-or-deleted-records.mjs @@ -4,10 +4,11 @@ import moment from "moment"; export default { ...base, name: "New, Modified or Deleted Records", + description: "Emit new event each time a record is added, updated, or deleted in an Airtable table. Supports tables up to 10,000 records", key: "airtable_oauth-new-modified-or-deleted-records", - version: "0.0.7", + version: "0.0.8", type: "source", - description: "Emit new event each time a record is added, updated, or deleted in an Airtable table. Supports tables up to 10,000 records", + dedupe: "unique", props: { ...base.props, tableId: { @@ -111,7 +112,7 @@ export default { id: recordID, }; this.$emit(deletedRecordObj, { - summary: "record_deleted", + summary: `Record deleted: ${recordID}`, id: recordID, }); } diff --git a/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs b/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs index 8ff6651c69f1c..4d7c6ea787d45 100644 --- a/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs +++ b/components/airtable_oauth/sources/new-or-modified-field/new-or-modified-field.mjs @@ -1,56 +1,38 @@ -import common from "../common/common.mjs"; +import common from "../common/common-webhook-field.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; export default { - name: "New or Modified Field", - description: "Emit new event for each new or modified field in a table", + ...common, + name: "New or Modified Field (Instant)", + description: "Emit new event when a field is created or updated in the selected table", key: "airtable_oauth-new-or-modified-field", - version: "0.0.7", + version: "1.0.0", type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + "update", + ]; + }, + }, props: { ...common.props, - tableId: { + watchSchemasOfFieldIds: { propDefinition: [ - common.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, + airtable, + "sortFieldId", + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, }), ], - description: "The table ID to watch for changes.", - }, - }, - methods: { - _getPrevFields() { - return this.db.get("fieldIds") || {}; + type: "string[]", + label: "Field Schemas to Watch", + description: + "Only emit events for updates that modify the schemas of these fields. If omitted, schemas of all fields within the table/view/base are watched", }, - _setPrevFields(fieldIds) { - this.db.set("fieldIds", fieldIds); - }, - }, - async run() { - const prevFields = this._getPrevFields(); - - const { tables } = await this.airtable.listTables({ - baseId: this.baseId, - }); - const { fields } = tables.find(({ id }) => id === this.tableId); - - for (const field of fields) { - if (prevFields[field.id] && prevFields[field.id] === JSON.stringify(field)) { - continue; - } - const summary = prevFields[field.id] - ? `${field.name} Updated` - : `${field.name} Created`; - prevFields[field.id] = JSON.stringify(field); - - this.$emit(field, { - id: field.id, - summary, - ts: Date.now(), - }); - } - - this._setPrevFields(prevFields); }, }; diff --git a/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs b/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs index c130c8cfc7921..1a70d3d604cbf 100644 --- a/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs +++ b/components/airtable_oauth/sources/new-or-modified-records-in-view/new-or-modified-records-in-view.mjs @@ -6,8 +6,9 @@ export default { name: "New or Modified Records in View", description: "Emit new event for each new or modified record in a view", key: "airtable_oauth-new-or-modified-records-in-view", - version: "0.0.7", + version: "0.0.8", type: "source", + dedupe: "unique", props: { ...base.props, tableId: { diff --git a/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs b/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs index 7bc083785dbad..3a4aade0d4a5f 100644 --- a/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs +++ b/components/airtable_oauth/sources/new-or-modified-records/new-or-modified-records.mjs @@ -1,155 +1,38 @@ -import base from "../common/common.mjs"; -import sampleEmit from "./test-event.mjs"; -import moment from "moment"; +import common from "../common/common-webhook-record.mjs"; +import airtable from "../../airtable_oauth.app.mjs"; export default { - ...base, - name: "New or Modified Records", + ...common, + name: "New or Modified Records (Instant)", key: "airtable_oauth-new-or-modified-records", - description: "Emit new event for each new or modified record in a table", - version: "0.0.9", + description: "Emit new event for each new or modified record in a table or view", + version: "1.0.0", type: "source", - props: { - ...base.props, - tableId: { - propDefinition: [ - base.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + "update", + ]; }, - fieldIds: { + }, + props: { + ...common.props, + watchDataInFieldIds: { propDefinition: [ - base.props.airtable, + airtable, "sortFieldId", - ({ - baseId, tableId, - }) => ({ - baseId, - tableId, + (c) => ({ + baseId: c.baseId, + tableId: c.tableId, }), ], type: "string[]", - label: "Field IDs", - description: "Identifiers of spedific fields/columns to watch for updates", - withLabel: true, - }, - returnFieldsByFieldId: { - propDefinition: [ - base.props.airtable, - "returnFieldsByFieldId", - ], - }, - }, - methods: { - ...base.methods, - _getFieldValues() { - return this.db.get("fieldValues") || {}; + label: "Watch Data In Field Ids", + description: + "Only emit events for updates that modify values in cells in these fields. If omitted, all fields within the table/view/base are watched", }, - _setFieldValues(fieldValues) { - this.db.set("fieldValues", fieldValues); - }, - updateFieldValues(newFieldValues, record) { - const fieldKey = this.returnFieldsByFieldId - ? "value" - : "label"; - for (const fieldId of this.fieldIds) { - newFieldValues[record.id] = { - ...newFieldValues[record.id], - [fieldId.value]: record.fields[fieldId[fieldKey]] || null, - }; - } - return newFieldValues; - }, - isUpdated(fieldValues, fieldIds, record) { - const fieldKey = this.returnFieldsByFieldId - ? "value" - : "label"; - for (const fieldId of fieldIds) { - if (!record.fields[fieldId[fieldKey]]) { - record.fields[fieldId[fieldKey]] = null; - } - if (fieldValues[record.id] - && fieldValues[record.id][fieldId.value] !== undefined - && record.fields[fieldId[fieldKey]] !== fieldValues[record.id][fieldId.value] - ) { - return true; - } - } - return false; - }, - }, - async run(event) { - const { - baseId, - tableId, - viewId, - } = this; - - const lastTimestamp = this._getLastTimestamp(); - const fieldValues = this._getFieldValues(); - const isFirstRunWithFields = this.fieldIds && Object.keys(fieldValues).length === 0; - const params = { - returnFieldsByFieldId: this.returnFieldsByFieldId || false, - }; - if (!isFirstRunWithFields) { - params.filterByFormula = `LAST_MODIFIED_TIME() > "${lastTimestamp}"`; - } - - const records = await this.airtable.listRecords({ - baseId, - tableId, - params, - }); - - if (!records.length) { - console.log("No new or modified records."); - return; - } - - const metadata = { - baseId, - tableId, - viewId, - }; - - let newRecords = 0, modifiedRecords = 0; - let newFieldValues = { - ...fieldValues, - }; - for (const record of records) { - if (this.fieldIds) { - newFieldValues = this.updateFieldValues(newFieldValues, record); - } - if (!lastTimestamp || moment(record.createdTime) > moment(lastTimestamp)) { - record.type = "new_record"; - newRecords++; - } else { - if (this.fieldIds - && (!this.isUpdated(fieldValues, this.fieldIds, record) || isFirstRunWithFields) - ) { - continue; - } - record.type = "record_modified"; - modifiedRecords++; - } - - record.metadata = metadata; - - this.$emit(record, { - summary: `${record.type}: ${JSON.stringify(record.fields)}`, - id: record.id, - ts: Date.now(), - }); - } - this._setFieldValues(newFieldValues); - console.log(`Emitted ${newRecords} new records(s) and ${modifiedRecords} modified record(s).`); - - // We keep track of the timestamp of the current invocation - this.updateLastTimestamp(event); }, - sampleEmit, }; diff --git a/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs b/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs deleted file mode 100644 index d33e5d1a470a7..0000000000000 --- a/components/airtable_oauth/sources/new-or-modified-records/test-event.mjs +++ /dev/null @@ -1,27 +0,0 @@ -export default { - "id": "recslHzC7AUjXDw", - "createdTime": "2023-07-31T04:29:26.000Z", - "fields": { - "Status": "In progress", - "ID": 27, - "Concat Calculation": "In progress-Medium", - "Record ID": "recslHzC7AUjXDw", - "Last Modified By": { - "id": "usrBLNP11LGUDJNTJ", - "email": "test@test.com", - "name": "Test User" - }, - "Created By": { - "id": "usrBLNP11LGUDJNTJ", - "email": "test@test.com", - "name": "Test User" - }, - "Name": "Pipedream 1129 Kanban", - "Priority": "Medium" - }, - "type": "record_modified", - "metadata": { - "baseId": "appeSZQLVSAD2cz", - "tableId": "tblPay0M5s9" - } - } \ No newline at end of file diff --git a/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs b/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs index 4132004cf6574..2e5cc68d869bc 100644 --- a/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs +++ b/components/airtable_oauth/sources/new-records-in-view/new-records-in-view.mjs @@ -6,8 +6,9 @@ export default { name: "New Records in View", description: "Emit new event for each new record in a view", key: "airtable_oauth-new-records-in-view", - version: "0.0.7", + version: "0.0.8", type: "source", + dedupe: "unique", props: { ...base.props, tableId: { @@ -78,7 +79,7 @@ export default { this.$emit(record, { ts: moment(record.createdTime).valueOf(), - summary: JSON.stringify(record.fields), + summary: `New record: ${record.id}`, id: record.id, }); if (!maxTimestamp || moment(record.createdTime).valueOf() > moment(maxTimestamp).valueOf()) { diff --git a/components/airtable_oauth/sources/new-records/new-records.mjs b/components/airtable_oauth/sources/new-records/new-records.mjs index b310ee45052b8..3cf18cc89e3c9 100644 --- a/components/airtable_oauth/sources/new-records/new-records.mjs +++ b/components/airtable_oauth/sources/new-records/new-records.mjs @@ -1,78 +1,19 @@ -import base from "../common/common.mjs"; -import moment from "moment"; +import common from "../common/common-webhook-record.mjs"; export default { - ...base, - name: "New Records", + ...common, + name: "New Record(s) Created (Instant)", description: "Emit new event for each new record in a table", key: "airtable_oauth-new-records", - version: "0.0.7", + version: "1.0.0", type: "source", - props: { - ...base.props, - tableId: { - propDefinition: [ - base.props.airtable, - "tableId", - ({ baseId }) => ({ - baseId, - }), - ], - description: "The table ID to watch for changes.", + dedupe: "unique", + methods: { + ...common.methods, + getChangeTypes() { + return [ + "add", + ]; }, - returnFieldsByFieldId: { - propDefinition: [ - base.props.airtable, - "returnFieldsByFieldId", - ], - }, - }, - async run() { - const { - baseId, - tableId, - viewId, - } = this; - - const lastTimestamp = this._getLastTimestamp(); - const params = { - filterByFormula: `CREATED_TIME() > "${lastTimestamp}"`, - returnFieldsByFieldId: this.returnFieldsByFieldId || false, - }; - - const records = await this.airtable.listRecords({ - baseId, - tableId, - params, - }); - - if (!records.length) { - console.log("No new records."); - return; - } - - const metadata = { - baseId, - tableId, - viewId, - }; - - let maxTimestamp; - let recordCount = 0; - for (const record of records) { - record.metadata = metadata; - - this.$emit(record, { - ts: moment(record.createdTime).valueOf(), - summary: JSON.stringify(record.fields), - id: record.id, - }); - if (!maxTimestamp || moment(record.createdTime).valueOf() > moment(maxTimestamp).valueOf()) { - maxTimestamp = record.createdTime; - } - recordCount++; - } - console.log(`Emitted ${recordCount} new records(s).`); - this._setLastTimestamp(maxTimestamp); }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b01adb56f3068..2a466a487cf4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,8 +94,8 @@ importers: specifier: ^12.3.4 version: 12.5.0(enquirer@2.4.1) pnpm: - specifier: 9.14.2 - version: 9.14.2 + specifier: 9.14.3 + version: 9.14.3 putout: specifier: '>=36' version: 36.13.1(eslint@8.57.1)(typescript@5.6.3) @@ -463,9 +463,6 @@ importers: bottleneck: specifier: ^2.19.5 version: 2.19.5 - lodash: - specifier: ^4.17.21 - version: 4.17.21 lodash.chunk: specifier: ^4.2.0 version: 4.2.0 @@ -1334,8 +1331,7 @@ importers: components/braze: {} - components/breathe: - specifiers: {} + components/breathe: {} components/brevo: dependencies: @@ -1625,8 +1621,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/change_photos: - specifiers: {} + components/change_photos: {} components/changenow: {} @@ -5395,8 +5390,7 @@ importers: specifier: ^1.5.1 version: 1.6.6 - components/kafka: - specifiers: {} + components/kafka: {} components/kajabi: {} @@ -8945,8 +8939,7 @@ importers: specifier: ^1.4.1 version: 1.6.6 - components/scrapegraphai: - specifiers: {} + components/scrapegraphai: {} components/scrapein_: {} @@ -10235,8 +10228,7 @@ importers: components/taggun: {} - components/taleez: - specifiers: {} + components/taleez: {} components/talend: {} @@ -23112,8 +23104,8 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - pnpm@9.14.2: - resolution: {integrity: sha512-biuvd9Brk2IpQVLIUcTyeO3jerHro6Vf2jF6SheyCfTbuXP7JQp3q8Rjo0H8sfF/F8+iQJHE6zGc2g2bhCeDhw==} + pnpm@9.14.3: + resolution: {integrity: sha512-wPU+6ZR37ZabgrKJrQEaXRa/FiPJV+fynqvo0MALV0wpuMf1T2xn7nEMc/KFyBVNB85EtG/iwO60dqkEQbrDcQ==} engines: {node: '>=18.12'} hasBin: true @@ -24573,22 +24565,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} @@ -40512,7 +40504,7 @@ snapshots: pluralize@8.0.0: {} - pnpm@9.14.2: {} + pnpm@9.14.3: {} points-on-curve@0.2.0: {}