├── .eslintrc.js ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── acceptance-test-config.yml ├── bin └── main ├── cloudbuild.yaml ├── package-lock.json ├── package.json ├── readme.md ├── resources ├── schemas │ ├── attachment.json │ ├── auditEntry.json │ ├── comment.json │ ├── customer.json │ ├── customerNeed.json │ ├── cycle.json │ ├── document.json │ ├── documentContent.json │ ├── entityExternalLink.json │ ├── initiative.json │ ├── initiativeToProject.json │ ├── issue.json │ ├── issueHistory.json │ ├── issueLabel.json │ ├── issueRelation.json │ ├── organization.json │ ├── project.json │ ├── projectMilestone.json │ ├── projectStatus.json │ ├── projectUpdate.json │ ├── team.json │ ├── teamKey.json │ ├── teamMembership.json │ ├── user.json │ └── workflowState.json └── spec.json ├── src ├── client │ ├── LinearClient.ts │ └── types.ts ├── index.ts ├── streams │ ├── attachment.ts │ ├── auditEntry.ts │ ├── comment.ts │ ├── customer.ts │ ├── customerNeed.ts │ ├── cycle.ts │ ├── document.ts │ ├── documentContent.ts │ ├── entityExternalLink.ts │ ├── index.ts │ ├── initiative.ts │ ├── initiativeToProject.ts │ ├── issue.ts │ ├── issueHistory.ts │ ├── issueLabel.ts │ ├── issueRelation.ts │ ├── organization.ts │ ├── project.ts │ ├── projectMilestone.ts │ ├── projectStatus.ts │ ├── projectUpdate.ts │ ├── team.ts │ ├── teamKey.ts │ ├── teamMembership.ts │ ├── user.ts │ └── workflowState.ts └── tsconfig.json └── test_files ├── config_template.json ├── full_configured_catalog.json └── invalid_config.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const typescriptRules = { 2 | "@typescript-eslint/array-type": [ 3 | "error", 4 | { 5 | default: "array", 6 | }, 7 | ], 8 | "@typescript-eslint/await-thenable": "warn", 9 | "@typescript-eslint/dot-notation": "warn", 10 | "@typescript-eslint/explicit-member-accessibility": [ 11 | "warn", 12 | { 13 | accessibility: "explicit", 14 | overrides: { 15 | accessors: "explicit", 16 | constructors: "explicit", 17 | parameterProperties: "explicit", 18 | }, 19 | }, 20 | ], 21 | "@typescript-eslint/indent": "off", 22 | "@typescript-eslint/member-ordering": "off", 23 | "@typescript-eslint/naming-convention": "off", 24 | "@typescript-eslint/no-empty-function": "warn", 25 | "@typescript-eslint/no-empty-interface": "off", 26 | "@typescript-eslint/no-explicit-any": "warn", 27 | "@typescript-eslint/no-floating-promises": "error", 28 | "@typescript-eslint/no-misused-promises": [ 29 | "warn", 30 | { 31 | checksVoidReturn: false, 32 | }, 33 | ], 34 | "@typescript-eslint/no-misused-new": "error", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "off", // Too many false positives. See https://github.com/typescript-eslint/typescript-eslint/issues/1410 36 | "@typescript-eslint/no-var-requires": "error", 37 | "@typescript-eslint/unified-signatures": "error", 38 | "@typescript-eslint/no-unused-vars": [ 39 | "warn", 40 | { 41 | vars: "all", 42 | args: "none", // "after-used", TODO: enable 43 | // argsIgnorePattern: "^_", 44 | ignoreRestSiblings: true, 45 | }, 46 | ], 47 | "@typescript-eslint/no-shadow": [ 48 | "warn", 49 | { 50 | hoist: "all", 51 | }, 52 | ], 53 | // Should fix 54 | "@typescript-eslint/ban-types": ["off"], // Todo: switch from tslint/ban 55 | "@typescript-eslint/no-inferrable-types": "off", // TODO: enable 56 | // Disable a few @typescript-eslint recommended rules 57 | "@typescript-eslint/explicit-module-boundary-types": "off", 58 | "@typescript-eslint/no-unsafe-assignment": "off", // TODO: enable 59 | "@typescript-eslint/no-unsafe-member-access": "off", // TODO: enable 60 | "@typescript-eslint/no-unsafe-call": "off", // TODO: enable 61 | "@typescript-eslint/no-unsafe-return": "off", // TODO: enable 62 | "@typescript-eslint/restrict-template-expressions": "off", // TODO: enable 63 | "@typescript-eslint/require-await": "off", // TODO: enable 64 | "@typescript-eslint/no-unsafe-return": "off", // TODO: enable 65 | "@typescript-eslint/prefer-regexp-exec": "off", // TODO: enable 66 | "@typescript-eslint/unbound-method": "off", // TODO: enable 67 | "@typescript-eslint/ban-ts-comment": "off", 68 | "@typescript-eslint/no-non-null-assertion": "off", 69 | "@typescript-eslint/restrict-plus-operands": "off", 70 | "@typescript-eslint/no-namespace": "off" 71 | }; 72 | const generalRules = { 73 | "prefer-spread": "off", // TODO: enable 74 | "constructor-super": "error", 75 | curly: "error", 76 | "default-case": "error", 77 | "eol-last": "off", 78 | eqeqeq: ["error", "always"], 79 | "id-blacklist": ["warn", "any", "String", "string", "Boolean", "boolean", "Undefined", "undefined"], 80 | "id-match": "error", 81 | "no-template-curly-in-string": "error", 82 | "no-undef-init": "warn", 83 | "no-underscore-dangle": "off", 84 | "no-var": "warn", 85 | "prefer-arrow-functions": "off", 86 | "prefer-const": "warn", 87 | "linebreak-style": "off", 88 | "no-caller": "error", 89 | "no-console": "warn", 90 | "no-debugger": "error", 91 | "no-duplicate-imports": "error", 92 | "no-empty": "error", 93 | "no-eval": "error", 94 | "no-invalid-this": "off", 95 | "no-null/no-null": "off", 96 | "no-redeclare": "off", 97 | "no-unused-vars": "off", 98 | }; 99 | const jsDocRules = { 100 | "jsdoc/check-alignment": "warn", 101 | "jsdoc/check-indentation": "off", // TODO: enable 102 | "jsdoc/newline-after-description": "warn", 103 | }; 104 | const importRules = { 105 | "no-restricted-imports": [ 106 | "error", 107 | { 108 | paths: [ 109 | { 110 | name: "typeorm", 111 | importNames: ["BaseEntity"], 112 | }, 113 | { 114 | name: "type-graphql", 115 | importNames: ["Mutation", "Query"], 116 | message: "Please use @AuthorizedMutation or @AuthorizedQuery instead (refer to resolvers/Decorators.ts)", 117 | }, 118 | ], 119 | patterns: ["^[\\./]+/common/", "@linear/*/src/*"], 120 | }, 121 | ], 122 | "import/no-cycle": "warn", 123 | "import/order": [ 124 | "warn", 125 | { 126 | pathGroups: [ 127 | { 128 | pattern: "~/**", 129 | group: "external", 130 | }, 131 | ], 132 | }, 133 | ], 134 | }; 135 | const reactRules = { 136 | "react/jsx-key": "off", 137 | "react/jsx-uses-vars": "warn", 138 | "react/jsx-uses-react": "warn", 139 | "react-hooks/rules-of-hooks": "error", 140 | "react-hooks/exhaustive-deps": "off", // Enable later 141 | }; 142 | const nodeRules = { 143 | "node/no-process-env": "error", 144 | }; 145 | 146 | const config = { 147 | env: { 148 | browser: true, 149 | es6: true, 150 | }, 151 | ignorePatterns: ["**/.eslintrc.js"], 152 | parser: "@typescript-eslint/parser", 153 | parserOptions: { 154 | project: "./src/tsconfig.json", 155 | tsconfigRootDir: ".", 156 | sourceType: "module", 157 | extraFileExtensions: [".json"], 158 | ecmaFeatures: { 159 | jsx: true, 160 | }, 161 | }, 162 | plugins: [ 163 | "@typescript-eslint", 164 | "@typescript-eslint/tslint", 165 | "eslint-plugin-jsdoc", 166 | "eslint-plugin-import", 167 | "eslint-plugin-no-null", 168 | "eslint-plugin-react", 169 | "eslint-plugin-node", 170 | "eslint-plugin-react-hooks", 171 | "import", 172 | ], 173 | extends: ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"], 174 | rules: { 175 | "prettier/prettier": "warn", 176 | ...typescriptRules, 177 | ...generalRules, 178 | ...jsDocRules, 179 | ...reactRules, 180 | ...importRules, 181 | ...nodeRules, 182 | }, 183 | overrides: [ 184 | { 185 | files: ["**/*.js"], 186 | env: { node: true }, 187 | parserOptions: { 188 | ecmaVersion: 6, 189 | sourceType: "script", 190 | ecmaFeatures: { 191 | jsx: true, 192 | }, 193 | }, 194 | extends: ["plugin:prettier/recommended"], 195 | rules: { 196 | ...generalRules, 197 | ...jsDocRules, 198 | ...nodeRules, 199 | "node/no-process-env": "off", 200 | "@typescript-eslint/no-var-requires": "off", 201 | }, 202 | }, 203 | ], 204 | }; 205 | 206 | module.exports = config; 207 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | test_files/config.json 4 | .pytest_cache 5 | acceptance_tests_logs -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Integration tests 2 | 3 | The source is tested against an Airbyte-provided docker image that runs a series of tests to validate all the commands of a source. 4 | 5 | The acceptance-test-config.yml points to several other json files that enable the tests for each of the source commands. See the (Source Acceptance Tests Reference)[https://docs.airbyte.com/connector-development/testing-connectors/source-acceptance-tests-reference/] for how those files are used. These files should be committed to the repo. 6 | 7 | ### Running tests locally 8 | 9 | In order to run tests locally follow the step: 10 | 11 | - copy `test_files/config_template.json` into `test_files/config.json` and replace `${LINEAR_AIRBYTE_KEY}` with an integration key for your workspace. See https://linear.app/docs/airbyte for more details 12 | - run `docker pull airbyte/source-acceptance-test` to download airbyte's docker image with integration test suite 13 | - build source connector docker image `docker build -t linear-airbyte-source .` 14 | - execute tests `docker run --rm -t -v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp -v $(pwd):/test_input airbyte/source-acceptance-test:latest --acceptance-test-config /test_input` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM node:16-alpine as builder 2 | WORKDIR /home/node/airbyte 3 | RUN npm install --location=global npm@7 tsc 4 | 5 | COPY package.json package-lock.json ./ 6 | RUN npm install 7 | 8 | COPY ./resources ./resources 9 | COPY ./src ./src 10 | COPY ./bin ./bin 11 | RUN npm run build 12 | 13 | FROM node:16-alpine 14 | WORKDIR /home/node/airbyte 15 | COPY --from=builder /home/node/airbyte /home/node/airbyte 16 | ENV AIRBYTE_ENTRYPOINT "/home/node/airbyte/bin/main" 17 | ENTRYPOINT ["/home/node/airbyte/bin/main"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Igor Sechyn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /acceptance-test-config.yml: -------------------------------------------------------------------------------- 1 | connector_image: linear-airbyte-source 2 | tests: 3 | spec: 4 | - config_path: "test_files/config.json" 5 | spec_path: "resources/spec.json" 6 | connection: 7 | - config_path: "test_files/config.json" 8 | status: "succeed" 9 | - config_path: "test_files/invalid_config.json" 10 | status: "failed" 11 | discovery: 12 | - config_path: "test_files/config.json" 13 | basic_read: 14 | - config_path: "test_files/config.json" 15 | configured_catalog_path: "test_files/full_configured_catalog.json" 16 | expect_trace_message_on_failure: false 17 | full_refresh: 18 | - config_path: "test_files/config.json" 19 | configured_catalog_path: "test_files/full_configured_catalog.json" 20 | -------------------------------------------------------------------------------- /bin/main: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {mainCommand} = require('../lib'); 4 | 5 | mainCommand().parseAsync(process.argv).catch((err) => { 6 | console.error(err.message); 7 | process.exit(1); 8 | }); 9 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 3 | args: ['run', '--privileged', 'linuxkit/binfmt:v0.7'] 4 | id: 'initialize-qemu' 5 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 6 | args: ['buildx', 'create', '--name', 'mybuilder'] 7 | id: 'create-builder' 8 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 9 | args: ['buildx', 'use', 'mybuilder'] 10 | id: 'select-builder' 11 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 12 | args: ['buildx', 'inspect', '--bootstrap'] 13 | id: 'show-target-build-platforms' 14 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 15 | entrypoint: "/bin/bash" 16 | secretEnv: ["LINEAR_AIRBYTE_KEY"] 17 | args: 18 | - "-c" 19 | - "-eEuo" 20 | - "pipefail" 21 | - | 22 | apt-get update 23 | apt-get install -qq -y gettext 24 | envsubst < test_files/config_template.json > test_files/config.json 25 | docker buildx build --platform=linux/amd64 -t linear-airbyte-source --load -f Dockerfile . 26 | docker run --rm -t -v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp -v $(pwd):/test_input airbyte/source-acceptance-test:latest --acceptance-test-config /test_input 27 | id: integration-test 28 | - name: 'gcr.io/cloud-builders/docker:20.10.3' 29 | entrypoint: "/bin/bash" 30 | args: 31 | - "-c" 32 | - | 33 | [[ "$BRANCH_NAME" == "main" ]] && docker buildx build --platform=$_DOCKER_BUILDX_PLATFORMS -t gcr.io/${PROJECT_ID}/linear-airbyte-source:$BUILD_ID -t gcr.io/${PROJECT_ID}/linear-airbyte-source:latest --push -f Dockerfile . || docker buildx build --platform=$_DOCKER_BUILDX_PLATFORMS -t gcr.io/${PROJECT_ID}/linear-airbyte-source:$BUILD_ID -t gcr.io/${PROJECT_ID}/linear-airbyte-source:latest -f Dockerfile . 34 | id: build-multi-architecture-container-image 35 | options: 36 | env: 37 | - DOCKER_CLI_EXPERIMENTAL=enabled 38 | substitutions: 39 | _DOCKER_BUILDX_PLATFORMS: 'linux/amd64,linux/arm64' 40 | availableSecrets: 41 | secretManager: 42 | - versionName: projects/$PROJECT_ID/secrets/linear_airbyte_key/versions/latest 43 | env: "LINEAR_AIRBYTE_KEY" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linear-airbyte-source", 3 | "version": "0.1.0", 4 | "description": "Linear Airbyte source", 5 | "keywords": [ 6 | "airbyte", 7 | "source", 8 | "faros", 9 | "linear" 10 | ], 11 | "author": "Linear Orbit Inc.", 12 | "license": "MIT", 13 | "files": [ 14 | "lib/", 15 | "resources/" 16 | ], 17 | "engines": { 18 | "node": ">=16.5" 19 | }, 20 | "main": "./lib", 21 | "scripts": { 22 | "build": "tsc -p src", 23 | "lint-staged": "lint-staged", 24 | "clean": "rm -rf lib node_modules lib", 25 | "fix": "prettier --write 'src/**/*.ts' && npm run lint -- --fix", 26 | "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", 27 | "watch": "tsc -b -w src test", 28 | "prepare": "husky install" 29 | }, 30 | "dependencies": { 31 | "axios": "0.26.0", 32 | "commander": "9.0.0", 33 | "faros-airbyte-cdk": "0.2.13", 34 | "ts-essentials": "9.1.2", 35 | "verror": "1.10.1" 36 | }, 37 | "jest": { 38 | "coverageDirectory": "out/coverage", 39 | "coveragePathIgnorePatterns": [ 40 | "/node_modules/", 41 | "/test/" 42 | ], 43 | "preset": "ts-jest", 44 | "testEnvironment": "node", 45 | "testPathIgnorePatterns": [ 46 | ".d.ts", 47 | ".js" 48 | ], 49 | "testTimeout": 10000, 50 | "globals": { 51 | "ts-jest": { 52 | "tsconfig": "test/tsconfig.json" 53 | } 54 | } 55 | }, 56 | "lint-staged": { 57 | "*.{js,ts,tsx,json,css,yaml}": [ 58 | "prettier --write", 59 | "git add" 60 | ], 61 | "*.{ts,tsx}": [ 62 | "eslint --max-warnings 0 --fix -c .eslintrc.precommit.json", 63 | "git add" 64 | ] 65 | }, 66 | "devDependencies": { 67 | "@types/jest": "28.1.1", 68 | "@types/node": "16.11.7", 69 | "@types/verror": "^1.10.5", 70 | "@typescript-eslint/eslint-plugin": "5.28.0", 71 | "@typescript-eslint/eslint-plugin-tslint": "5.4.0", 72 | "@typescript-eslint/parser": "5.4.0", 73 | "eslint": "8.15.0", 74 | "eslint-config-prettier": "8.5.0", 75 | "eslint-plugin-import": "2.26.0", 76 | "eslint-plugin-jsdoc": "39.2.9", 77 | "eslint-plugin-no-null": "1.0.2", 78 | "eslint-plugin-node": "11.1.0", 79 | "eslint-plugin-prefer-arrow": "1.2.3", 80 | "eslint-plugin-prettier": "4.0.0", 81 | "eslint-plugin-react": "7.29.4", 82 | "eslint-plugin-react-hooks": "4.5.0", 83 | "husky": "7.0.4", 84 | "lint-staged": "13.0.1", 85 | "prettier": "2.6.2", 86 | "ts-jest": "28.0.4", 87 | "typescript": "4.7.3" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Linear 2 | 3 | ## Sync overview 4 | 5 | This source can sync data for the [Linear API](https://developers.linear.app/docs/). It supports only Full Refresh syncs. 6 | 7 | ### Output schema 8 | 9 | This Source is capable of syncing the following Streams: 10 | 11 | - [Issue](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Issue) 12 | - [Organization](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Organization) 13 | - [Team](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Team) 14 | - [User](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/User) 15 | - [ProjectMilestone](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/ProjectMilestone) 16 | - [Project](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Project) 17 | - [ProjectStatus](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/ProjectStatus) 18 | - [Initiative](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Initiative) 19 | - [InitiativeToProject](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/InitiativeToProject) 20 | - [Integration Resource](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/IntegrationResource) 21 | - [Attachment](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Attachment) 22 | - [Audit Entry](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/AuditEntry) 23 | - [Comment](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Comment) 24 | - [Cycle](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Cycle) 25 | - [Workflow State](https://github.com/linear/linear/blob/master/packages/sdk/src/schema.graphql#L12325) 26 | - [Document](https://github.com/linear/linear/blob/master/packages/sdk/src/schema.graphql#L1438) 27 | - [DocumentContent](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/DocumentContent) 28 | - [EntityExternalLink](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/EntityExternalLink) 29 | - [IssueHistory](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/IssueHistory) 30 | - [IssueLabel](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/IssueLabel) 31 | - [IssueRelation](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/IssueRelation) 32 | - [ProjectUpdate](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/ProjectUpdate) 33 | - [TeamKey](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/TeamKey) 34 | - [TeamMembership](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/TeamMembership) 35 | - [Customer](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/Customer) 36 | - [CustomerNeed](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference/objects/CustomerNeed) 37 | 38 | ### Data type mapping 39 | 40 | | Integration Type | Airbyte Type | Notes | 41 | | :----------------------- | :----------- | :---- | 42 | | `string` | `string` | | 43 | | `int`, `float`, `number` | `number` | | 44 | | `date` | `date` | | 45 | | `datetime` | `datetime` | | 46 | | `array` | `array` | | 47 | | `object` | `object` | | 48 | 49 | ### Features 50 | 51 | | Feature | Supported?\(Yes/No\) | Notes | 52 | | :---------------- | :------------------- | :---- | 53 | | Full Refresh Sync | Yes | | 54 | | Incremental Sync | No | | 55 | | Namespaces | No | | 56 | 57 | ### Performance considerations 58 | 59 | The connector is restricted by Linear's Data Export API, which is only available to the Plus paid plan. 60 | 61 | The Linear connector should not run into Linear API limitations under normal usage. Please [create an issue](https://github.com/airbytehq/airbyte/issues) if you see any rate limit issues that are not automatically retried successfully. 62 | 63 | ## Getting started 64 | 65 | ### Requirements 66 | 67 | - Integration API Token (generated from the [settings](https://linear.app/settings/integrations/airbyte) page) 68 | - Linear Enterprise plan subscription 69 | - Docker [desktop](https://www.docker.com/products/docker-desktop/) 70 | 71 | ### Setup guide 72 | 73 | Please follow these details [steps](https://linear.app/docs/airbyte). 74 | 75 | ## Changelog 76 | 77 | | Version | Date | Pull Request | Subject | 78 | | :------ | :--- | :----------- | :------ | 79 | -------------------------------------------------------------------------------- /resources/schemas/attachment.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "title": { 17 | "type": "string" 18 | }, 19 | "subtitle": { 20 | "type":["null", "string"] 21 | }, 22 | "url": { 23 | "type": "string" 24 | }, 25 | "creatorId": { 26 | "type": ["null", "string"] 27 | }, 28 | "issueId": { 29 | "type": "string" 30 | }, 31 | "metadata": { 32 | "type": "object" 33 | }, 34 | "source": { 35 | "type": "object" 36 | }, 37 | "groupBySource": { 38 | "type": "boolean" 39 | }, 40 | "iconUploadId": { 41 | "type": [ "null", "string" ] 42 | }, 43 | "sourceMetadata": { 44 | "type": [ "null", "object" ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/schemas/auditEntry.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "type": { 17 | "type": "string" 18 | }, 19 | "organizationId": { 20 | "type": "string" 21 | }, 22 | "actorId": { 23 | "type": ["null", "string"] 24 | }, 25 | "ip": { 26 | "type": [ "string", "null" ] 27 | }, 28 | "countryCode": { 29 | "type": [ "null", "string" ] 30 | }, 31 | "metadata": { 32 | "type": "object" 33 | }, 34 | "userId": { 35 | "type": [ "null", "string" ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/schemas/comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "resolvedAt": { 17 | "type": ["null", "string"] 18 | }, 19 | "issueId": { 20 | "type": ["null", "string"] 21 | }, 22 | "resolvingCommentId": { 23 | "type": ["null", "string"] 24 | }, 25 | "resolvingUserId": { 26 | "type": ["null", "string"] 27 | }, 28 | "parentId": { 29 | "type": ["null", "string"] 30 | }, 31 | "userId": { 32 | "type": ["null", "string"] 33 | }, 34 | "editedAt": { 35 | "type": ["null", "string"] 36 | }, 37 | "attachmentId": { 38 | "type": ["null", "string"] 39 | }, 40 | "sourceMetadata": { 41 | "type": ["null", "object"] 42 | }, 43 | "bodyData": { 44 | "type": ["null", "string"] 45 | }, 46 | "reactionData": { 47 | "type": "array", 48 | "items": { 49 | "type": "object" 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/schemas/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "organizationId": { 17 | "type": "string" 18 | }, 19 | "domains": { 20 | "type": "array", 21 | "items": { 22 | "type": "string" 23 | } 24 | }, 25 | "externalIds": { 26 | "type": "array", 27 | "items": { 28 | "type": "string" 29 | } 30 | }, 31 | "name": { 32 | "type": "string" 33 | }, 34 | "logoUrl": { 35 | "type": [ "null", "string" ] 36 | }, 37 | "slugId": { 38 | "type": "string" 39 | }, 40 | "statusId": { 41 | "type": "string" 42 | }, 43 | "tierId": { 44 | "type": [ "null", "string" ] 45 | }, 46 | "slackChannelId": { 47 | "type": [ "null", "string" ] 48 | }, 49 | "revenue": { 50 | "type": [ "null", "integer" ] 51 | }, 52 | "size": { 53 | "type": [ "null", "integer" ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /resources/schemas/customerNeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "organizationId": { 17 | "type": "string" 18 | }, 19 | "customerId": { 20 | "type": [ "null", "string" ] 21 | }, 22 | "issueId": { 23 | "type": [ "null", "string" ] 24 | }, 25 | "projectId": { 26 | "type": [ "null", "string" ] 27 | }, 28 | "attachmentId": { 29 | "type": [ "null", "string" ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/schemas/cycle.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "number": { 17 | "type": "integer" 18 | }, 19 | "name": { 20 | "type": [ "null", "string" ] 21 | }, 22 | "startsAt": { 23 | "type": "string" 24 | }, 25 | "endsAt": { 26 | "type": "string" 27 | }, 28 | "completedAt": { 29 | "type": [ "null", "string" ] 30 | }, 31 | "autoArchivedAt": { 32 | "type": [ "null", "string" ] 33 | }, 34 | "issueCountHistory": { 35 | "type": "array", 36 | "items": { 37 | "type": "integer" 38 | } 39 | }, 40 | "completedIssueCountHistory": { 41 | "type": "array", 42 | "items": { 43 | "type": "integer" 44 | } 45 | }, 46 | "scopeHistory": { 47 | "type": "array", 48 | "items": { 49 | "type": "integer" 50 | } 51 | }, 52 | "completedScopeHistory": { 53 | "type": "array", 54 | "items": { 55 | "type": "integer" 56 | } 57 | }, 58 | "teamId": { 59 | "type": "string" 60 | }, 61 | "uncompletedIssuesUponCloseIds": { 62 | "type": "array", 63 | "items": { 64 | "type": "string" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /resources/schemas/document.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "title": { 17 | "type": "string" 18 | }, 19 | "icon": { 20 | "type": ["null", "string"] 21 | }, 22 | "color": { 23 | "type": ["null", "string"] 24 | }, 25 | "creatorId": { 26 | "type": "string" 27 | }, 28 | "updatedById": { 29 | "type": "string" 30 | }, 31 | "projectId": { 32 | "type": ["null", "string"] 33 | }, 34 | "slugId": { 35 | "type": "string" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/schemas/documentContent.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "organizationId": { 8 | "type": "string" 9 | }, 10 | "createdAt": { 11 | "type": "string" 12 | }, 13 | "updatedAt": { 14 | "type": "string" 15 | }, 16 | "archivedAt": { 17 | "type": ["null", "string"] 18 | }, 19 | "content": { 20 | "type": ["null", "string"] 21 | }, 22 | "projectId": { 23 | "type": ["null", "string"] 24 | }, 25 | "issueId": { 26 | "type": ["null", "string"] 27 | }, 28 | "documentId": { 29 | "type": ["null", "string"] 30 | }, 31 | "projectMilestoneId": { 32 | "type": ["null", "string"] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/schemas/entityExternalLink.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "url": { 17 | "type": "string" 18 | }, 19 | "label": { 20 | "type": "string" 21 | }, 22 | "creatorId": { 23 | "type": "string" 24 | }, 25 | "projectId": { 26 | "type": ["null", "string"] 27 | }, 28 | "initiativeId": { 29 | "type": ["null", "string"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/schemas/initiative.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "organizationId": { 8 | "type": "string" 9 | }, 10 | "name": { 11 | "type": "string" 12 | }, 13 | "description": { 14 | "type": ["string", "null"] 15 | }, 16 | "createdAt": { 17 | "type": "string" 18 | }, 19 | "updatedAt": { 20 | "type": "string" 21 | }, 22 | "archivedAt": { 23 | "type": ["null", "string"] 24 | }, 25 | "creatorId": { 26 | "type": "string" 27 | }, 28 | "slugId": { 29 | "type": "string" 30 | }, 31 | "ownerId": { 32 | "type": ["null", "string"] 33 | }, 34 | "status": { 35 | "type": "string" 36 | }, 37 | "icon": { 38 | "type": ["null", "string"] 39 | }, 40 | "color": { 41 | "type": ["null", "string"] 42 | }, 43 | "targetDate": { 44 | "type": ["null", "string"] 45 | }, 46 | "targetDateResolution": { 47 | "type": ["null", "string"] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /resources/schemas/initiativeToProject.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "projectId": { 17 | "type": "string" 18 | }, 19 | "initiativeId": { 20 | "type": "string" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/schemas/issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "number": { 17 | "type": "integer" 18 | }, 19 | "title": { 20 | "type": "string" 21 | }, 22 | "priority": { 23 | "type": "integer" 24 | }, 25 | "estimate": { 26 | "type": ["null", "integer"] 27 | }, 28 | "boardOrder": { 29 | "type": "number" 30 | }, 31 | "sortOrder": { 32 | "type": "number" 33 | }, 34 | "startedAt": { 35 | "type": ["null", "string"] 36 | }, 37 | "completedAt": { 38 | "type": ["null", "string"] 39 | }, 40 | "canceledAt": { 41 | "type": ["null", "string"] 42 | }, 43 | "autoClosedAt": { 44 | "type": ["null", "string"] 45 | }, 46 | "autoArchivedAt": { 47 | "type": ["null", "string"] 48 | }, 49 | "dueDate": { 50 | "type": ["null", "string"] 51 | }, 52 | "trashed": { 53 | "type": ["null", "boolean"] 54 | }, 55 | "snoozedUntilAt": { 56 | "type": ["null", "string"] 57 | }, 58 | "labelIds": { 59 | "type": "array", 60 | "items": { 61 | "type": "string" 62 | } 63 | }, 64 | "teamId": { 65 | "type": "string" 66 | }, 67 | "cycleId": { 68 | "type": ["null", "string"] 69 | }, 70 | "projectId": { 71 | "type": ["null", "string"] 72 | }, 73 | "projectMilestoneId": { 74 | "type": ["null", "string"] 75 | }, 76 | "subscriberIds": { 77 | "type": "array", 78 | "items": { 79 | "type": "string" 80 | } 81 | }, 82 | "previousIdentifiers": { 83 | "type": "array", 84 | "items": {} 85 | }, 86 | "creatorId": { 87 | "type": ["null", "string"] 88 | }, 89 | "assigneeId": { 90 | "type": ["null", "string"] 91 | }, 92 | "snoozedById": { 93 | "type": ["null", "string"] 94 | }, 95 | "issueImportId": { 96 | "type": ["null", "string"] 97 | }, 98 | "stateId": { 99 | "type": "string" 100 | }, 101 | "parentId": { 102 | "type": ["null", "string"] 103 | }, 104 | "subIssueSortOrder": { 105 | "type": ["null", "number"] 106 | }, 107 | "sourceMetadata": { 108 | "type": ["null", "object"] 109 | }, 110 | "documentVersion": { 111 | "type": "integer" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /resources/schemas/issueHistory.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "issueId": { 17 | "type": "string" 18 | }, 19 | "actorId": { 20 | "type": ["null", "string"] 21 | }, 22 | "source": { 23 | "type": ["null", "string"] 24 | }, 25 | "updatedDescription": { 26 | "type": ["null", "boolean"] 27 | }, 28 | "fromTitle": { 29 | "type": ["null", "string"] 30 | }, 31 | "toTitle": { 32 | "type": ["null", "string"] 33 | }, 34 | "fromAssigneeId": { 35 | "type": ["null", "string"] 36 | }, 37 | "toAssigneeId": { 38 | "type": ["null", "string"] 39 | }, 40 | "fromPriority": { 41 | "type": ["null", "integer"] 42 | }, 43 | "toPriority": { 44 | "type": ["null", "integer"] 45 | }, 46 | "fromTeamId": { 47 | "type": ["null", "string"] 48 | }, 49 | "toTeamId": { 50 | "type": ["null", "string"] 51 | }, 52 | "fromParentId": { 53 | "type": ["null", "string"] 54 | }, 55 | "toParentId": { 56 | "type": ["null", "string"] 57 | }, 58 | "fromStateId": { 59 | "type": ["null", "string"] 60 | }, 61 | "toStateId": { 62 | "type": ["null", "string"] 63 | }, 64 | "fromCycleId": { 65 | "type": ["null", "string"] 66 | }, 67 | "toCycleId": { 68 | "type": ["null", "string"] 69 | }, 70 | "fromProjectId": { 71 | "type": ["null", "string"] 72 | }, 73 | "toProjectId": { 74 | "type": ["null", "string"] 75 | }, 76 | "fromEstimate": { 77 | "type": ["null", "integer"] 78 | }, 79 | "toEstimate": { 80 | "type": ["null", "integer"] 81 | }, 82 | "archived": { 83 | "type": ["null", "boolean"] 84 | }, 85 | "trashed": { 86 | "type": ["null", "boolean"] 87 | }, 88 | "issueImportId": { 89 | "type": ["null", "string"] 90 | }, 91 | "attachmentId": { 92 | "type": ["null", "string"] 93 | }, 94 | "addedLabelIds": { 95 | "type": ["null", "array"] 96 | }, 97 | "removedLabelIds": { 98 | "type": ["null", "array"] 99 | }, 100 | "relationChanges": { 101 | "type": ["null", "array"] 102 | }, 103 | "autoClosed": { 104 | "type": ["null", "boolean"] 105 | }, 106 | "autoArchived": { 107 | "type": ["null", "boolean"] 108 | }, 109 | "fromDueDate": { 110 | "type": ["null", "string"] 111 | }, 112 | "toDueDate": { 113 | "type": ["null", "string"] 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /resources/schemas/issueLabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "description": { 20 | "type": ["null", "string"] 21 | }, 22 | "color": { 23 | "type": "string" 24 | }, 25 | "organizationId": { 26 | "type": ["null", "string"] 27 | }, 28 | "teamId": { 29 | "type": ["null", "string"] 30 | }, 31 | "creatorId": { 32 | "type": ["null", "string"] 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /resources/schemas/issueRelation.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "type": { 17 | "type": "string" 18 | }, 19 | "issueId": { 20 | "type": "string" 21 | }, 22 | "relatedIssueId": { 23 | "type": "string" 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /resources/schemas/organization.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "urlKey": { 20 | "type": "string" 21 | }, 22 | "logoUrl": { 23 | "type": ["null", "string"] 24 | }, 25 | "periodUploadVolume": { 26 | "type": "integer" 27 | }, 28 | "gitBranchFormat": { 29 | "type": ["null", "string"] 30 | }, 31 | "gitLinkbackMessagesEnabled": { 32 | "type": "boolean" 33 | }, 34 | "gitPublicLinkbackMessagesEnabled": { 35 | "type": "boolean" 36 | }, 37 | "roadmapEnabled": { 38 | "type": "boolean" 39 | }, 40 | "samlEnabled": { 41 | "type": "boolean" 42 | }, 43 | "allowedAuthServices": { 44 | "type": "array", 45 | "items": {} 46 | }, 47 | "deletionRequestedAt": { 48 | "type": ["null", "string"] 49 | }, 50 | "reducedPersonalInformation": { 51 | "type": "boolean" 52 | }, 53 | "enabled": { 54 | "type": "boolean" 55 | }, 56 | "deletionRequestedById": { 57 | "type": ["null", "string"] 58 | }, 59 | "serviceId": { 60 | "type": "string" 61 | }, 62 | "linearPreviewFlags": { 63 | "type": "object" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /resources/schemas/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "statusId": { 8 | "type": "string" 9 | }, 10 | "createdAt": { 11 | "type": "string" 12 | }, 13 | "updatedAt": { 14 | "type": "string" 15 | }, 16 | "archivedAt": { 17 | "type": ["null", "string"] 18 | }, 19 | "name": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": ["null", "string"] 24 | }, 25 | "slugId": { 26 | "type": "string" 27 | }, 28 | "icon": { 29 | "type": ["null", "string"] 30 | }, 31 | "color": { 32 | "type": "string" 33 | }, 34 | "state": { 35 | "type": "string" 36 | }, 37 | "teamIds": { 38 | "type": "array", 39 | "items": { 40 | "type": "string" 41 | } 42 | }, 43 | "creatorId": { 44 | "type": "string" 45 | }, 46 | "leadId": { 47 | "type": ["null", "string"] 48 | }, 49 | "memberIds": { 50 | "type": "array", 51 | "items": { 52 | "type": "string" 53 | } 54 | }, 55 | "organizationId": { 56 | "type": "string" 57 | }, 58 | "milestoneId": { 59 | "type": ["null", "string"] 60 | }, 61 | "lastProjectUpdatePromptAt": { 62 | "type": ["null", "string"] 63 | }, 64 | "startDate": { 65 | "type": ["null", "string"] 66 | }, 67 | "targetDate": { 68 | "type": ["null", "string"] 69 | }, 70 | "startedAt": { 71 | "type": ["null", "string"] 72 | }, 73 | "completedAt": { 74 | "type": ["null", "string"] 75 | }, 76 | "canceledAt": { 77 | "type": ["null", "string"] 78 | }, 79 | "autoArchivedAt": { 80 | "type": ["null", "string"] 81 | }, 82 | "sortOrder": { 83 | "type": "number" 84 | }, 85 | "issueCountHistory": { 86 | "type": "array", 87 | "items": { 88 | "type": "integer" 89 | } 90 | }, 91 | "completedIssueCountHistory": { 92 | "type": "array", 93 | "items": { 94 | "type": "integer" 95 | } 96 | }, 97 | "scopeHistory": { 98 | "type": "array", 99 | "items": { 100 | "type": "integer" 101 | } 102 | }, 103 | "completedScopeHistory": { 104 | "type": "array", 105 | "items": { 106 | "type": "integer" 107 | } 108 | }, 109 | "slackNewIssue": { 110 | "type": "boolean" 111 | }, 112 | "slackIssueComments": { 113 | "type": "boolean" 114 | }, 115 | "slackIssueStatuses": { 116 | "type": "boolean" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /resources/schemas/projectMilestone.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "name": { 8 | "type": "string" 9 | }, 10 | "createdAt": { 11 | "type": "string" 12 | }, 13 | "updatedAt": { 14 | "type": "string" 15 | }, 16 | "archivedAt": { 17 | "type": ["null", "string"] 18 | }, 19 | "targetDate": { 20 | "type": ["null", "string"] 21 | }, 22 | "projectId": { 23 | "type": "string" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/schemas/projectStatus.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "organizationId": { 8 | "type": "string" 9 | }, 10 | "createdAt": { 11 | "type": "string" 12 | }, 13 | "updatedAt": { 14 | "type": "string" 15 | }, 16 | "archivedAt": { 17 | "type": [ "null", "string" ] 18 | }, 19 | "name": { 20 | "type": "string" 21 | }, 22 | "color": { 23 | "type": "string" 24 | }, 25 | "description": { 26 | "type": [ "null", "string" ] 27 | }, 28 | "position": { 29 | "type": "number" 30 | }, 31 | "type": { 32 | "type": "string" 33 | }, 34 | "indefinite": { 35 | "type": "boolean" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/schemas/projectUpdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "projectId": { 17 | "type": "string" 18 | }, 19 | "health": { 20 | "type": "string" 21 | }, 22 | "userId": { 23 | "type": "string" 24 | }, 25 | "editedAt": { 26 | "type": ["null", "string"] 27 | }, 28 | "reactionData": { 29 | "type": "array", 30 | "items": { 31 | "type": "object" 32 | } 33 | }, 34 | "bodyData": { 35 | "type": ["null", "string"] 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /resources/schemas/team.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "key": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": ["null", "string"] 24 | }, 25 | "icon": { 26 | "type": ["null", "string"] 27 | }, 28 | "color": { 29 | "type": "string" 30 | }, 31 | "organizationId": { 32 | "type": "string" 33 | }, 34 | "cyclesEnabled": { 35 | "type": "boolean" 36 | }, 37 | "cycleStartDay": { 38 | "type": "integer" 39 | }, 40 | "cycleDuration": { 41 | "type": "integer" 42 | }, 43 | "cycleCooldownTime": { 44 | "type": "integer" 45 | }, 46 | "cycleIssueAutoAssignStarted": { 47 | "type": "boolean" 48 | }, 49 | "cycleIssueAutoAssignCompleted": { 50 | "type": "boolean" 51 | }, 52 | "cycleLockToActive": { 53 | "type": "boolean" 54 | }, 55 | "upcomingCycleCount": { 56 | "type": "integer" 57 | }, 58 | "timezone": { 59 | "type": "string" 60 | }, 61 | "inviteHash": { 62 | "type": "string" 63 | }, 64 | "issueEstimationType": { 65 | "type": "string" 66 | }, 67 | "issueOrderingNoPriorityFirst": { 68 | "type": "boolean" 69 | }, 70 | "issueEstimationAllowZero": { 71 | "type": "boolean" 72 | }, 73 | "issueSortOrderDefaultToBottom": { 74 | "type": "boolean" 75 | }, 76 | "issueEstimationExtended": { 77 | "type": "boolean" 78 | }, 79 | "defaultIssueEstimate": { 80 | "type": "integer" 81 | }, 82 | "triageEnabled": { 83 | "type": "boolean" 84 | }, 85 | "defaultIssueStateId": { 86 | "type": ["null", "string"] 87 | }, 88 | "defaultTemplateForMembersId": { 89 | "type": ["null", "string"] 90 | }, 91 | "defaultTemplateForNonMembersId": { 92 | "type": ["null", "string"] 93 | }, 94 | "triageIssueStateId": { 95 | "type": ["null", "string"] 96 | }, 97 | "private": { 98 | "type": "boolean" 99 | }, 100 | "draftWorkflowStateId": { 101 | "type": ["null", "string"] 102 | }, 103 | "startWorkflowStateId": { 104 | "type": ["null", "string"] 105 | }, 106 | "reviewWorkflowStateId": { 107 | "type": ["null", "string"] 108 | }, 109 | "mergeWorkflowStateId": { 110 | "type": ["null", "string"] 111 | }, 112 | "groupIssueHistory": { 113 | "type": "boolean" 114 | }, 115 | "slackNewIssue": { 116 | "type": "boolean" 117 | }, 118 | "slackIssueComments": { 119 | "type": "boolean" 120 | }, 121 | "slackIssueStatuses": { 122 | "type": "boolean" 123 | }, 124 | "autoClosePeriod": { 125 | "type": ["null", "integer"] 126 | }, 127 | "autoCloseStateId": { 128 | "type": ["null", "string"] 129 | }, 130 | "autoArchivePeriod": { 131 | "type": "integer" 132 | }, 133 | "markedAsDuplicateWorkflowStateId": { 134 | "type": ["null", "string"] 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /resources/schemas/teamKey.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "teamId": { 20 | "type": "string" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /resources/schemas/teamMembership.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "userId": { 17 | "type": "string" 18 | }, 19 | "teamId": { 20 | "type": "string" 21 | }, 22 | "owner": { 23 | "type": "boolean" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /resources/schemas/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": ["null", "string"] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "displayName": { 20 | "type": "string" 21 | }, 22 | "email": { 23 | "type": "string" 24 | }, 25 | "avatarUrl": { 26 | "type": ["null", "string"] 27 | }, 28 | "disableReason": { 29 | "type": ["null", "string"] 30 | }, 31 | "inviteHash": { 32 | "type": ["null", "string"] 33 | }, 34 | "description": { 35 | "type": ["null", "string"] 36 | }, 37 | "statusEmoji": { 38 | "type": ["null", "string"] 39 | }, 40 | "statusLabel": { 41 | "type": ["null", "string"] 42 | }, 43 | "statusUntilAt": { 44 | "type": ["null", "string"] 45 | }, 46 | "timezone": { 47 | "type": ["null", "string"] 48 | }, 49 | "organizationId": { 50 | "type": "string" 51 | }, 52 | "userAccountId": { 53 | "type": "string" 54 | }, 55 | "alternativeEmails": { 56 | "type": "array", 57 | "items": {} 58 | }, 59 | "gitHubUserId": { 60 | "type": ["null", "string"] 61 | }, 62 | "externalUserMapping": { 63 | "type": "object" 64 | }, 65 | "external": { 66 | "type": "boolean" 67 | }, 68 | "role": { 69 | "type": "string" 70 | }, 71 | "active": { 72 | "type": "boolean" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/schemas/workflowState.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "properties": { 4 | "id": { 5 | "type": "string" 6 | }, 7 | "createdAt": { 8 | "type": "string" 9 | }, 10 | "updatedAt": { 11 | "type": "string" 12 | }, 13 | "archivedAt": { 14 | "type": [ "null", "string" ] 15 | }, 16 | "name": { 17 | "type": "string" 18 | }, 19 | "color": { 20 | "type": "string" 21 | }, 22 | "description": { 23 | "type": [ "null", "string" ] 24 | }, 25 | "position": { 26 | "type": "number" 27 | }, 28 | "type": { 29 | "type": "string" 30 | }, 31 | "teamId": { 32 | "type": "string" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "documentationUrl": "https://developers.linear.app/docs/", 3 | "supportsIncremental": false, 4 | "connectionSpecification": { 5 | "$schema": "http://json-schema.org/draft-07/schema#", 6 | "title": "Linear Spec", 7 | "type": "object", 8 | "required": ["apiKey"], 9 | "additionalProperties": true, 10 | "properties": { 11 | "apiKey": { 12 | "type": "string", 13 | "title": "Airbyte Integration API Key", 14 | "airbyte_secret": true 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/client/LinearClient.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { AirbyteLogger } from "faros-airbyte-cdk/lib"; 3 | import { 4 | Attachment, 5 | AuditEntry, 6 | Comment, 7 | Customer, 8 | CustomerNeed, 9 | Document, 10 | DocumentContent, 11 | EntityExternalLink, 12 | Initiative, 13 | InitiativeToProject, 14 | Issue, 15 | IssueHistory, 16 | IssueLabel, 17 | IssueRelation, 18 | Organization, 19 | Project, 20 | ProjectMilestone, 21 | ProjectStatus, 22 | ProjectUpdate, 23 | Team, 24 | TeamKey, 25 | TeamMembership, 26 | User, 27 | WorkflowState, 28 | } from "./types"; 29 | 30 | const LINEAR_API_BASE_URL = "https://api.linear.app/export/"; 31 | 32 | /** 33 | * Linear client configuration 34 | */ 35 | export type Config = { 36 | // access token provided in linear airbyte integration 37 | apiKey: string; 38 | }; 39 | 40 | /* 41 | * The supported entity types. 42 | */ 43 | export type EntityType = 44 | | "issue" 45 | | "organization" 46 | | "team" 47 | | "teamkey" 48 | | "teammembership" 49 | | "user" 50 | | "entityexternallink" 51 | | "project" 52 | | "projectstatus" 53 | | "projectupdate" 54 | | "projectmilestone" 55 | | "initiative" 56 | | "initiativetoproject" 57 | | "issuehistory" 58 | | "issuelabel" 59 | | "issuerelation" 60 | | "attachment" 61 | | "auditentry" 62 | | "cycle" 63 | | "workflowstate" 64 | | "document" 65 | | "documentcontent" 66 | | "comment" 67 | | "customer" 68 | | "customerneed"; 69 | /** 70 | * Thin client on top of the rest export api to fetch different resources. 71 | */ 72 | export class LinearClient { 73 | public constructor( 74 | private readonly config: Config, 75 | protected readonly logger: AirbyteLogger 76 | ) {} 77 | 78 | /** 79 | * @returns List of all issues in organization. 80 | */ 81 | public async issues(): Promise { 82 | return await this.fetchEntities("issue"); 83 | } 84 | 85 | /** 86 | * @returns List of all teams in organization. 87 | */ 88 | public async teams(): Promise { 89 | return await this.fetchEntities("team"); 90 | } 91 | 92 | /** 93 | * @returns List of all users in organization. 94 | */ 95 | public async users(): Promise { 96 | return await this.fetchEntities("user"); 97 | } 98 | 99 | /** 100 | * @returns List of all team keys in organization. 101 | */ 102 | public async teamKeys(): Promise { 103 | return await this.fetchEntities("teamkey"); 104 | } 105 | 106 | /** 107 | * @returns List of all team memberships in organization. 108 | */ 109 | public async teamMemberships(): Promise { 110 | return await this.fetchEntities("teammembership"); 111 | } 112 | 113 | /** 114 | * @returns List of all projects in organization. 115 | */ 116 | public async projects(): Promise { 117 | return await this.fetchEntities("project"); 118 | } 119 | 120 | /** 121 | * @returns List of all projects in organization. 122 | */ 123 | public async projectStatuses(): Promise { 124 | return await this.fetchEntities("projectstatus"); 125 | } 126 | 127 | /** 128 | * @returns List of all projects in organization. 129 | */ 130 | public async projectUpdates(): Promise { 131 | return await this.fetchEntities("projectupdate"); 132 | } 133 | 134 | /** 135 | * @returns List of all project milestones in organization. 136 | */ 137 | public async projectMilestones(): Promise { 138 | return await this.fetchEntities("projectmilestone"); 139 | } 140 | /** 141 | * @returns List of all project links in organization. 142 | */ 143 | public async entityExternalLinks(): Promise { 144 | return await this.fetchEntities("entityexternallink"); 145 | } 146 | 147 | /** 148 | * @returns List of all issue history entries in organization. 149 | */ 150 | public async issueHistory(): Promise { 151 | return await this.fetchEntities("issuehistory"); 152 | } 153 | 154 | /** 155 | * @returns List of all issue labels in organization. 156 | */ 157 | public async issueLabels(): Promise { 158 | return await this.fetchEntities("issuelabel"); 159 | } 160 | 161 | /** 162 | * @returns List of all issue relations in organization. 163 | */ 164 | public async issueRelations(): Promise { 165 | return await this.fetchEntities("issuerelation"); 166 | } 167 | 168 | /** 169 | * @returns List of all attachments in organization. 170 | */ 171 | public async attachments(): Promise { 172 | return await this.fetchEntities("attachment"); 173 | } 174 | 175 | /** 176 | * @returns List of all audit entries in organization. 177 | */ 178 | public async auditEntries(): Promise { 179 | return await this.fetchEntities("auditentry"); 180 | } 181 | 182 | /** 183 | * @returns List of all comments in organization. 184 | */ 185 | public async comments(): Promise { 186 | return await this.fetchEntities("comment"); 187 | } 188 | 189 | /** 190 | * @returns List of all cycles in organization. 191 | */ 192 | public async cycles(): Promise { 193 | return await this.fetchEntities("cycle"); 194 | } 195 | 196 | /** 197 | * @returns List of all workflow states in organization. 198 | */ 199 | public async workflowStates(): Promise { 200 | return await this.fetchEntities("workflowstate"); 201 | } 202 | 203 | /** 204 | * @returns List of all documents in organization. 205 | */ 206 | public async documents(): Promise { 207 | return await this.fetchEntities("document"); 208 | } 209 | 210 | /** 211 | * @returns List of all initiatives in organization. 212 | */ 213 | public async initiatives(): Promise { 214 | return await this.fetchEntities("initiative"); 215 | } 216 | 217 | /** 218 | * @returns List of all initiative to project relations in organization. 219 | */ 220 | public async initiativeToProject(): Promise { 221 | return await this.fetchEntities("initiativetoproject"); 222 | } 223 | 224 | /** 225 | * @returns List of all document content entities in organization. 226 | */ 227 | public async documentContents(): Promise { 228 | return await this.fetchEntities("documentcontent"); 229 | } 230 | 231 | /** 232 | * @returns List of all Customer entities in organization. 233 | */ 234 | public async customer(): Promise { 235 | return await this.fetchEntities("customer"); 236 | } 237 | 238 | /** 239 | * @returns List of all CustomerNeed entities in organization. 240 | */ 241 | public async customerNeed(): Promise { 242 | return await this.fetchEntities("customerneed"); 243 | } 244 | 245 | /** 246 | * 247 | * @returns Organization associated with api token. The response will always contain a single organization. 248 | */ 249 | public async organizations(): Promise { 250 | return await this.fetchEntities("organization"); 251 | } 252 | 253 | public async checkConnection(): Promise { 254 | await axios({ 255 | method: "GET", 256 | baseURL: LINEAR_API_BASE_URL, 257 | url: "checkConnection", 258 | headers: { 259 | Authorization: this.config.apiKey, 260 | }, 261 | }); 262 | } 263 | 264 | private async fetchEntities(entityType: EntityType): Promise { 265 | const response = await axios({ 266 | method: "GET", 267 | baseURL: LINEAR_API_BASE_URL, 268 | url: entityType, 269 | headers: { 270 | Authorization: this.config.apiKey, 271 | }, 272 | }); 273 | return response.data; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/client/types.ts: -------------------------------------------------------------------------------- 1 | export type Issue = { 2 | id: string; 3 | createdAt: string; 4 | updatedAt: string; 5 | archivedAt: null | string; 6 | number: number; 7 | title: string; 8 | priority: number; 9 | estimate?: null | string; 10 | boardOrder?: number; 11 | sortOrder?: number; 12 | startedAt?: null | string; 13 | completedAt?: null | string; 14 | canceledAt?: null | string; 15 | autoClosedAt?: null | string; 16 | autoArchivedAt?: null | string; 17 | dueDate?: null | string; 18 | trashed?: null | string; 19 | snoozedUntilAt?: null | string; 20 | labelIds: string[]; 21 | teamId: string; 22 | cycleId?: null | string; 23 | projectId?: null | string; 24 | projectMilestoneId?: null | string; 25 | subscriberIds?: string[]; 26 | previousIdentifiers?: string[]; 27 | creatorId: string; 28 | assigneeId: string; 29 | snoozedById?: null | string; 30 | issueImportId?: null | string; 31 | stateId?: string; 32 | parentId?: null | string; 33 | subIssueSortOrder?: null | string; 34 | sourceMetadata?: null | string; 35 | documentVersion?: number; 36 | }; 37 | 38 | export type Organization = { 39 | id: string; 40 | createdAt: string; 41 | updatedAt: string; 42 | archivedAt?: null | string; 43 | name: string; 44 | urlKey: string; 45 | logoUrl?: string; 46 | periodUploadVolume?: number; 47 | gitBranchFormat?: null | string; 48 | gitLinkbackMessagesEnabled?: boolean; 49 | gitPublicLinkbackMessagesEnabled?: boolean; 50 | roadmapEnabled?: boolean; 51 | samlEnabled?: boolean; 52 | allowedAuthServices?: unknown[]; 53 | deletionRequestedAt?: null | string; 54 | reducedPersonalInformation?: boolean; 55 | enabled: boolean; 56 | deletionRequestedById?: null | string; 57 | serviceId?: string; 58 | linearPreviewFlags?: { 59 | [k: string]: unknown; 60 | }; 61 | }; 62 | 63 | export interface Team { 64 | id?: string; 65 | createdAt?: string; 66 | updatedAt?: string; 67 | archivedAt?: null | string; 68 | name?: string; 69 | key?: string; 70 | description?: null | string; 71 | icon?: string; 72 | color?: string; 73 | organizationId?: string; 74 | cyclesEnabled?: boolean; 75 | cycleStartDay?: number; 76 | cycleDuration?: number; 77 | cycleCooldownTime?: number; 78 | cycleIssueAutoAssignStarted?: boolean; 79 | cycleIssueAutoAssignCompleted?: boolean; 80 | cycleLockToActive?: boolean; 81 | upcomingCycleCount?: number; 82 | timezone?: string; 83 | inviteHash?: string; 84 | issueEstimationType?: string; 85 | issueOrderingNoPriorityFirst?: boolean; 86 | issueEstimationAllowZero?: boolean; 87 | issueSortOrderDefaultToBottom?: boolean; 88 | issueEstimationExtended?: boolean; 89 | defaultIssueEstimate?: number; 90 | triageEnabled?: boolean; 91 | defaultIssueStateId?: string; 92 | defaultTemplateForMembersId?: null | string; 93 | defaultTemplateForNonMembersId?: null | string; 94 | triageIssueStateId?: null | string; 95 | private?: boolean; 96 | draftWorkflowStateId?: null | string; 97 | startWorkflowStateId?: string; 98 | reviewWorkflowStateId?: null | string; 99 | mergeWorkflowStateId?: string; 100 | groupIssueHistory?: boolean; 101 | slackNewIssue?: boolean; 102 | slackIssueComments?: boolean; 103 | slackIssueStatuses?: boolean; 104 | autoClosePeriod?: number; 105 | autoCloseStateId?: string; 106 | autoArchivePeriod?: number; 107 | markedAsDuplicateWorkflowStateId?: null | string; 108 | } 109 | 110 | export interface IssueHistory { 111 | id?: string; 112 | createdAt?: string; 113 | updatedAt?: string; 114 | archivedAt?: null | string; 115 | issueId?: string; 116 | actorId?: string; 117 | source?: null | string; 118 | updatedDescription?: null | string; 119 | fromTitle?: null | string; 120 | toTitle?: null | string; 121 | fromAssigneeId?: null | string; 122 | toAssigneeId?: null | string; 123 | fromPriority?: null | string; 124 | toPriority?: null | string; 125 | fromTeamId?: null | string; 126 | toTeamId?: null | string; 127 | fromParentId?: null | string; 128 | toParentId?: null | string; 129 | fromStateId?: null | string; 130 | toStateId?: null | string; 131 | fromCycleId?: null | string; 132 | toCycleId?: null | string; 133 | fromProjectId?: null | string; 134 | toProjectId?: null | string; 135 | fromEstimate?: null | string; 136 | toEstimate?: null | string; 137 | archived?: null | string; 138 | trashed?: null | string; 139 | issueImportId?: null | string; 140 | attachmentId?: null | string; 141 | addedLabelIds?: null | string; 142 | removedLabelIds?: null | string; 143 | relationChanges?: null | string; 144 | autoClosed?: boolean; 145 | autoArchived?: boolean; 146 | fromDueDate?: null | string; 147 | toDueDate?: null | string; 148 | } 149 | 150 | export interface IssueLabel { 151 | id?: string; 152 | createdAt?: string; 153 | updatedAt?: string; 154 | archivedAt?: null | string; 155 | name?: string; 156 | description?: null | string; 157 | color?: string; 158 | organizationId?: string; 159 | teamId?: null | string; 160 | creatorId?: null | string; 161 | } 162 | 163 | export interface Project { 164 | id?: string; 165 | statusId?: string; 166 | createdAt?: string; 167 | updatedAt?: string; 168 | archivedAt?: null | string; 169 | name?: string; 170 | description?: string; 171 | slugId?: string; 172 | icon?: null | string; 173 | color?: string; 174 | state?: string; 175 | teamIds?: string[]; 176 | creatorId?: string; 177 | leadId?: null | string; 178 | memberIds?: string[]; 179 | organizationId?: string; 180 | lastProjectUpdatePromptAt?: null | string; 181 | startDate?: string; 182 | targetDate?: string; 183 | startedAt?: string; 184 | completedAt?: null | string; 185 | canceledAt?: null | string; 186 | autoArchivedAt?: null | string; 187 | sortOrder?: number; 188 | issueCountHistory?: number[]; 189 | completedIssueCountHistory?: number[]; 190 | scopeHistory?: number[]; 191 | completedScopeHistory?: number[]; 192 | slackNewIssue?: boolean; 193 | slackIssueComments?: boolean; 194 | slackIssueStatuses?: boolean; 195 | } 196 | 197 | export interface ProjectMilestone { 198 | name?: string; 199 | id?: string; 200 | createdAt?: string; 201 | updatedAt?: string; 202 | archivedAt?: null | string; 203 | targetDate?: null | string; 204 | projectId?: string; 205 | } 206 | 207 | export interface Initiative { 208 | name?: string; 209 | id?: string; 210 | slugId?: string; 211 | createdAt?: string; 212 | updatedAt?: string; 213 | archivedAt?: null | string; 214 | targetDate?: null | string; 215 | targetDateResolution?: null | string; 216 | icon?: null | string; 217 | color?: null | string; 218 | description?: null | string; 219 | creatorId?: null | string; 220 | ownerId?: null | string; 221 | organizationId?: string; 222 | status?: string; 223 | } 224 | 225 | export interface InitiativeToProject { 226 | id?: string; 227 | createdAt?: string; 228 | updatedAt?: string; 229 | archivedAt?: null | string; 230 | initiativeId?: string; 231 | projectId?: string; 232 | } 233 | 234 | export interface EntityExternalLink { 235 | id?: string; 236 | createdAt?: string; 237 | updatedAt?: string; 238 | archivedAt?: null | string; 239 | url?: string; 240 | label?: string; 241 | creatorId?: string; 242 | projectId?: string; 243 | initiativeId?: string; 244 | } 245 | 246 | export interface ProjectUpdate { 247 | id?: string; 248 | createdAt?: string; 249 | updatedAt?: string; 250 | archivedAt?: null | string; 251 | projectId?: string; 252 | health?: string; 253 | userId?: string; 254 | editedAt?: string; 255 | reactionData?: unknown[]; 256 | bodyData?: string; 257 | } 258 | 259 | export interface TeamKey { 260 | id?: string; 261 | createdAt?: string; 262 | updatedAt?: string; 263 | archivedAt?: null | string; 264 | name?: string; 265 | teamId?: string; 266 | } 267 | 268 | export interface TeamMembership { 269 | id?: string; 270 | createdAt?: string; 271 | updatedAt?: string; 272 | archivedAt?: null | string; 273 | userId?: string; 274 | teamId?: string; 275 | owner?: boolean; 276 | } 277 | 278 | export interface User { 279 | id?: string; 280 | createdAt?: string; 281 | updatedAt?: string; 282 | archivedAt?: null | string; 283 | name?: string; 284 | displayName?: string; 285 | email?: string; 286 | avatarUrl?: string; 287 | disableReason?: null | string; 288 | inviteHash?: string; 289 | description?: null | string; 290 | statusEmoji?: null | string; 291 | statusLabel?: null | string; 292 | statusUntilAt?: null | string; 293 | timezone?: string; 294 | organizationId?: string; 295 | userAccountId?: string; 296 | alternativeEmails?: unknown[]; 297 | gitHubUserId?: null | string; 298 | externalUserMapping?: { 299 | [k: string]: unknown; 300 | }; 301 | external?: boolean; 302 | role?: string; 303 | active?: boolean; 304 | } 305 | 306 | export interface IssueRelation { 307 | id?: string; 308 | createdAt?: string; 309 | updatedAt?: string; 310 | archivedAt?: null; 311 | type?: string; 312 | issueId?: string; 313 | relatedIssueId?: string; 314 | } 315 | 316 | export interface Attachment { 317 | id?: string; 318 | createdAt?: string; 319 | updatedAt?: string; 320 | archivedAt?: null | string; 321 | title?: string; 322 | subtitle?: string; 323 | url?: string; 324 | creatorId?: string; 325 | issueId?: string; 326 | metadata?: { 327 | [k: string]: unknown; 328 | }; 329 | source?: { 330 | [k: string]: unknown; 331 | }; 332 | groupBySource?: boolean; 333 | iconUploadId?: null | string; 334 | sourceMetadata?: null | { 335 | [k: string]: unknown; 336 | }; 337 | } 338 | 339 | export interface AuditEntry { 340 | id?: string; 341 | createdAt?: string; 342 | updatedAt?: string; 343 | archivedAt?: null | string; 344 | type?: string; 345 | organizationId?: string; 346 | actorId?: string; 347 | ip?: string; 348 | countryCode?: null | string; 349 | metadata?: { 350 | [k: string]: unknown; 351 | }; 352 | userId?: null | string; 353 | [k: string]: unknown; 354 | } 355 | 356 | export interface Comment { 357 | id?: string; 358 | createdAt?: string; 359 | updatedAt?: string; 360 | archivedAt?: null | string; 361 | resolvedAt?: null | string; 362 | issueId?: string; 363 | parentId?: null | string; 364 | userId?: null | string; 365 | resolvingUserId?: null | string; 366 | resolvingCommentId?: null | string; 367 | editedAt?: string; 368 | attachmentId?: null | string; 369 | sourceMetadata?: null | { 370 | [k: string]: unknown; 371 | }; 372 | bodyData?: string; 373 | reactionData?: { 374 | [k: string]: unknown; 375 | }[]; 376 | } 377 | 378 | export interface Cycle { 379 | id?: string; 380 | createdAt?: string; 381 | updatedAt?: string; 382 | archivedAt?: null | string; 383 | number?: number; 384 | name?: null | string; 385 | startsAt?: string; 386 | endsAt?: string; 387 | completedAt?: null | string; 388 | autoArchivedAt?: null | string; 389 | issueCountHistory?: number[]; 390 | completedIssueCountHistory?: number[]; 391 | scopeHistory?: number[]; 392 | completedScopeHistory?: number[]; 393 | teamId?: string; 394 | uncompletedIssuesUponCloseIds?: number[]; 395 | } 396 | 397 | export interface WorkflowState { 398 | id?: string; 399 | createdAt?: string; 400 | updatedAt?: string; 401 | archivedAt?: null | string; 402 | name?: string; 403 | color?: string; 404 | description?: null | string; 405 | position?: number; 406 | type?: string; 407 | teamId?: string; 408 | } 409 | 410 | export interface ProjectStatus { 411 | id?: string; 412 | createdAt?: string; 413 | updatedAt?: string; 414 | archivedAt?: null | string; 415 | name?: string; 416 | color?: string; 417 | description?: null | string; 418 | position?: number; 419 | type?: string; 420 | organizationId?: string; 421 | indefinite?: boolean; 422 | } 423 | 424 | export interface Document { 425 | id?: string; 426 | createdAt?: string; 427 | updatedAt?: string; 428 | archivedAt?: null | string; 429 | title?: string; 430 | icon?: string; 431 | color?: string; 432 | creatorId?: string; 433 | updatedById?: string; 434 | projectId?: null | string; 435 | slugId?: string; 436 | } 437 | 438 | export interface DocumentContent { 439 | id?: string; 440 | organizationId: string; 441 | content?: string; 442 | issueId?: string; 443 | projectId?: string; 444 | documentId?: string; 445 | projectMilestoneId?: string; 446 | createdAt?: string; 447 | updatedAt?: string; 448 | archivedAt?: null | string; 449 | } 450 | 451 | export interface Customer { 452 | id?: string; 453 | createdAt: string; 454 | updatedAt: string; 455 | archivedAt?: null | string; 456 | organizationId: string; 457 | name: string; 458 | domains: string[]; 459 | externalIds: string[]; 460 | statusId: string; 461 | tierId?: string; 462 | logoUrl?: string; 463 | slackChannelId?: string; 464 | slugId: string; 465 | revenue?: number; 466 | size?: number; 467 | } 468 | 469 | export interface CustomerNeed { 470 | id: string; 471 | createdAt: string; 472 | updatedAt: string; 473 | archivedAt?: null | string; 474 | organizationId: string; 475 | customerId?: string; 476 | issueId?: string; 477 | projectId?: string; 478 | attachmentId?: string; 479 | } 480 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { 3 | AirbyteConfig, 4 | AirbyteLogger, 5 | AirbyteSourceBase, 6 | AirbyteSourceRunner, 7 | AirbyteSpec, 8 | AirbyteStreamBase, 9 | Spec, 10 | } from "faros-airbyte-cdk"; 11 | import VError from "verror"; 12 | import { LinearClient } from "./client/LinearClient"; 13 | 14 | import { Issue } from "./streams"; 15 | import { Attachment } from "./streams/attachment"; 16 | import { AuditEntry } from "./streams/auditEntry"; 17 | import { Comment } from "./streams/comment"; 18 | import { Cycle } from "./streams/cycle"; 19 | import { Document } from "./streams/document"; 20 | import { IssueHistory } from "./streams/issueHistory"; 21 | import { IssueLabel } from "./streams/issueLabel"; 22 | import { IssueRelation } from "./streams/issueRelation"; 23 | import { Organization } from "./streams/organization"; 24 | import { Project } from "./streams/project"; 25 | import { ProjectUpdate } from "./streams/projectUpdate"; 26 | import { Team } from "./streams/team"; 27 | import { TeamKey } from "./streams/teamKey"; 28 | import { TeamMembership } from "./streams/teamMembership"; 29 | import { User } from "./streams/user"; 30 | import { WorkflowState } from "./streams/workflowState"; 31 | import { DocumentContent } from "./streams/documentContent"; 32 | import { ProjectMilestone } from "./streams/projectMilestone"; 33 | import { ProjectStatus } from "./streams/projectStatus"; 34 | import { Initiative } from "./streams/initiative"; 35 | import { InitiativeToProject } from "./streams/initiativeToProject"; 36 | import { EntityExternalLink } from "./streams/entityExternalLink"; 37 | import { Customer } from "./streams/customer"; 38 | import { CustomerNeed } from "./streams/customerNeed"; 39 | 40 | /** The main entry point. */ 41 | export function mainCommand(): Command { 42 | const logger = new AirbyteLogger(); 43 | const source = new LinearSource(logger); 44 | return new AirbyteSourceRunner(logger, source).mainCommand(); 45 | } 46 | 47 | // eslint-disable-next-line @typescript-eslint/no-var-requires 48 | const spec: Spec = require("../resources/spec.json"); 49 | 50 | class LinearSource extends AirbyteSourceBase { 51 | public async spec(): Promise { 52 | return new AirbyteSpec(spec); 53 | } 54 | 55 | public async checkConnection( 56 | config: AirbyteConfig 57 | ): Promise<[boolean, VError]> { 58 | const client = new LinearClient({ apiKey: config.apiKey }, this.logger); 59 | try { 60 | await client.checkConnection(); 61 | return [true, null]; 62 | } catch (err) { 63 | return [false, new VError({ cause: err as Error }, "Error", {})]; 64 | } 65 | } 66 | 67 | public streams(config: AirbyteConfig): AirbyteStreamBase[] { 68 | const client = new LinearClient({ apiKey: config.apiKey }, this.logger); 69 | return [ 70 | new Issue(this.logger, client), 71 | new Organization(this.logger, client), 72 | new Team(this.logger, client), 73 | new TeamKey(this.logger, client), 74 | new TeamMembership(this.logger, client), 75 | new Initiative(this.logger, client), 76 | new InitiativeToProject(this.logger, client), 77 | new User(this.logger, client), 78 | new EntityExternalLink(this.logger, client), 79 | new Project(this.logger, client), 80 | new ProjectStatus(this.logger, client), 81 | new ProjectUpdate(this.logger, client), 82 | new ProjectMilestone(this.logger, client), 83 | new IssueHistory(this.logger, client), 84 | new IssueLabel(this.logger, client), 85 | new IssueRelation(this.logger, client), 86 | new Attachment(this.logger, client), 87 | new AuditEntry(this.logger, client), 88 | new Comment(this.logger, client), 89 | new Cycle(this.logger, client), 90 | new WorkflowState(this.logger, client), 91 | new Document(this.logger, client), 92 | new DocumentContent(this.logger, client), 93 | new Customer(this.logger, client), 94 | new CustomerNeed(this.logger, client), 95 | ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/streams/attachment.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Attachment as Model } from "../client/types"; 5 | 6 | export class Attachment extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/attachment.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.attachments(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/auditEntry.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { AuditEntry as Model } from "../client/types"; 5 | 6 | export class AuditEntry extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/auditEntry.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.auditEntries(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/comment.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Comment as Model } from "../client/types"; 5 | 6 | export class Comment extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/comment.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.comments(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/customer.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Customer as Model } from "../client/types"; 5 | 6 | export class Customer extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/customer.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | public async *readRecords(): AsyncGenerator { 23 | const result = await this.client.customer(); 24 | for (const record of result) { 25 | yield record; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/streams/customerNeed.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { CustomerNeed as Model } from "../client/types"; 5 | 6 | export class CustomerNeed extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/customerNeed.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | public async *readRecords(): AsyncGenerator { 23 | const result = await this.client.customerNeed(); 24 | for (const record of result) { 25 | yield record; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/streams/cycle.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Cycle as Model } from "../client/types"; 5 | 6 | export class Cycle extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/cycle.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.cycles(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/document.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Document as Model } from "../client/types"; 5 | 6 | export class Document extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/document.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.documents(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/documentContent.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { DocumentContent as Model } from "../client/types"; 5 | 6 | export class DocumentContent extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/documentContent.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.documentContents(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/entityExternalLink.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { EntityExternalLink as Model } from "../client/types"; 5 | export class EntityExternalLink extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | public getJsonSchema(): Dictionary { 13 | return require("../../resources/schemas/entityExternalLink.json"); 14 | } 15 | public get primaryKey(): StreamKey { 16 | return ["id"]; 17 | } 18 | public get cursorField(): string | string[] { 19 | return []; 20 | } 21 | public async *readRecords(): AsyncGenerator { 22 | const result = await this.client.entityExternalLinks(); 23 | for (const record of result) { 24 | yield record; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/streams/index.ts: -------------------------------------------------------------------------------- 1 | import { Issue } from "./issue"; 2 | export { Issue }; 3 | -------------------------------------------------------------------------------- /src/streams/initiative.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Initiative as Model } from "../client/types"; 5 | 6 | export class Initiative extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/initiative.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.initiatives(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/initiativeToProject.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { InitiativeToProject as Model } from "../client/types"; 5 | 6 | export class InitiativeToProject extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/initiativeToProject.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.initiativeToProject(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/issue.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Issue as IssueModel } from "../client/types"; 5 | export class Issue extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/issue.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | 23 | public async *readRecords(): AsyncGenerator { 24 | const result = await this.client.issues(); 25 | for (const record of result) { 26 | yield record; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/streams/issueHistory.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { IssueHistory as IssueHistoryModel } from "../client/types"; 5 | export class IssueHistory extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/issueHistory.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | 23 | public async *readRecords(): AsyncGenerator { 24 | const result = await this.client.issueHistory(); 25 | for (const record of result) { 26 | yield record; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/streams/issueLabel.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { IssueLabel as IssueLabelModel } from "../client/types"; 5 | export class IssueLabel extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/issueLabel.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | 23 | public async *readRecords(): AsyncGenerator { 24 | const result = await this.client.issueLabels(); 25 | for (const record of result) { 26 | yield record; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/streams/issueRelation.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { IssueRelation as Model } from "../client/types"; 5 | export class IssueRelation extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/issueRelation.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | public get cursorField(): string | string[] { 20 | return []; 21 | } 22 | 23 | public async *readRecords(): AsyncGenerator { 24 | const result = await this.client.issueRelations(); 25 | for (const record of result) { 26 | yield record; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/streams/organization.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Organization as OrganizationModel } from "../client/types"; 5 | export class Organization extends AirbyteStreamBase { 6 | public constructor( 7 | protected readonly logger: AirbyteLogger, 8 | private readonly client: LinearClient 9 | ) { 10 | super(logger); 11 | } 12 | 13 | public getJsonSchema(): Dictionary { 14 | return require("../../resources/schemas/organization.json"); 15 | } 16 | public get primaryKey(): StreamKey { 17 | return ["id"]; 18 | } 19 | 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.organizations(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/project.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Project as Model } from "../client/types"; 5 | 6 | export class Project extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/project.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.projects(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/projectMilestone.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { ProjectMilestone as Model } from "../client/types"; 5 | 6 | export class ProjectMilestone extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/projectMilestone.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.projectMilestones(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/projectStatus.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Project as Model } from "../client/types"; 5 | 6 | export class ProjectStatus extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/projectStatus.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.projectStatuses(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/projectUpdate.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { ProjectUpdate as Model } from "../client/types"; 5 | 6 | export class ProjectUpdate extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/projectUpdate.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.projectUpdates(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/team.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { Team as Model } from "../client/types"; 5 | 6 | export class Team extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/team.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.teams(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/teamKey.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { TeamKey as Model } from "../client/types"; 5 | 6 | export class TeamKey extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/teamKey.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.teamKeys(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/teamMembership.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { TeamMembership as Model } from "../client/types"; 5 | 6 | export class TeamMembership extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/teamMembership.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.teamMemberships(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/user.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { User as Model } from "../client/types"; 5 | 6 | export class User extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/user.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.users(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/streams/workflowState.ts: -------------------------------------------------------------------------------- 1 | import { AirbyteLogger, AirbyteStreamBase, StreamKey } from "faros-airbyte-cdk"; 2 | import { Dictionary } from "ts-essentials"; 3 | import { LinearClient } from "../client/LinearClient"; 4 | import { WorkflowState as Model } from "../client/types"; 5 | 6 | export class WorkflowState extends AirbyteStreamBase { 7 | public constructor( 8 | protected readonly logger: AirbyteLogger, 9 | private readonly client: LinearClient 10 | ) { 11 | super(logger); 12 | } 13 | 14 | public getJsonSchema(): Dictionary { 15 | return require("../../resources/schemas/workflowState.json"); 16 | } 17 | public get primaryKey(): StreamKey { 18 | return ["id"]; 19 | } 20 | public get cursorField(): string | string[] { 21 | return []; 22 | } 23 | 24 | public async *readRecords(): AsyncGenerator { 25 | const result = await this.client.workflowStates(); 26 | for (const record of result) { 27 | yield record; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "noImplicitAny": false, 7 | "pretty": true, 8 | "resolveJsonModule": false, 9 | "skipLibCheck": true, 10 | "sourceMap": false, 11 | "strict": true, 12 | "strictNullChecks": false, 13 | "target": "es2019", 14 | "composite": true, 15 | "outDir": "../lib" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test_files/config_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "${LINEAR_AIRBYTE_KEY}" 3 | } -------------------------------------------------------------------------------- /test_files/full_configured_catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "streams": [ 3 | { 4 | "stream": { 5 | "name": "attachment", 6 | "json_schema": {}, 7 | "supported_sync_modes": ["full_refresh"], 8 | "source_defined_cursor": true, 9 | "source_defined_primary_key": [["id"]] 10 | }, 11 | "sync_mode": "full_refresh", 12 | "destination_sync_mode": "overwrite" 13 | }, 14 | { 15 | "stream": { 16 | "name": "audit_entry", 17 | "json_schema": {}, 18 | "supported_sync_modes": ["full_refresh"], 19 | "source_defined_cursor": true, 20 | "source_defined_primary_key": [["id"]] 21 | }, 22 | "sync_mode": "full_refresh", 23 | "destination_sync_mode": "overwrite" 24 | }, 25 | { 26 | "stream": { 27 | "name": "comment", 28 | "json_schema": {}, 29 | "supported_sync_modes": ["full_refresh"], 30 | "source_defined_cursor": true, 31 | "source_defined_primary_key": [["id"]] 32 | }, 33 | "sync_mode": "full_refresh", 34 | "destination_sync_mode": "overwrite" 35 | }, 36 | { 37 | "stream": { 38 | "name": "initiative", 39 | "json_schema": {}, 40 | "supported_sync_modes": ["full_refresh"], 41 | "source_defined_cursor": true, 42 | "source_defined_primary_key": [["id"]] 43 | }, 44 | "sync_mode": "full_refresh", 45 | "destination_sync_mode": "overwrite" 46 | }, 47 | { 48 | "stream": { 49 | "name": "initiative_to_project", 50 | "json_schema": {}, 51 | "supported_sync_modes": ["full_refresh"], 52 | "source_defined_cursor": true, 53 | "source_defined_primary_key": [["id"]] 54 | }, 55 | "sync_mode": "full_refresh", 56 | "destination_sync_mode": "overwrite" 57 | }, 58 | { 59 | "stream": { 60 | "name": "document", 61 | "json_schema": {}, 62 | "supported_sync_modes": ["full_refresh"], 63 | "source_defined_cursor": true, 64 | "source_defined_primary_key": [["id"]] 65 | }, 66 | "sync_mode": "full_refresh", 67 | "destination_sync_mode": "overwrite" 68 | }, 69 | { 70 | "stream": { 71 | "name": "issue", 72 | "json_schema": {}, 73 | "supported_sync_modes": ["full_refresh"], 74 | "source_defined_cursor": true, 75 | "source_defined_primary_key": [["id"]] 76 | }, 77 | "sync_mode": "full_refresh", 78 | "destination_sync_mode": "overwrite" 79 | }, 80 | { 81 | "stream": { 82 | "name": "issue_history", 83 | "json_schema": {}, 84 | "supported_sync_modes": ["full_refresh"], 85 | "source_defined_cursor": true, 86 | "source_defined_primary_key": [["id"]] 87 | }, 88 | "sync_mode": "full_refresh", 89 | "destination_sync_mode": "overwrite" 90 | }, 91 | { 92 | "stream": { 93 | "name": "issue_label", 94 | "json_schema": {}, 95 | "supported_sync_modes": ["full_refresh"], 96 | "source_defined_cursor": true, 97 | "source_defined_primary_key": [["id"]] 98 | }, 99 | "sync_mode": "full_refresh", 100 | "destination_sync_mode": "overwrite" 101 | }, 102 | { 103 | "stream": { 104 | "name": "issue_relation", 105 | "json_schema": {}, 106 | "supported_sync_modes": ["full_refresh"], 107 | "source_defined_cursor": true, 108 | "source_defined_primary_key": [["id"]] 109 | }, 110 | "sync_mode": "full_refresh", 111 | "destination_sync_mode": "overwrite" 112 | }, 113 | { 114 | "stream": { 115 | "name": "project", 116 | "json_schema": {}, 117 | "supported_sync_modes": ["full_refresh"], 118 | "source_defined_cursor": true, 119 | "source_defined_primary_key": [["id"]] 120 | }, 121 | "sync_mode": "full_refresh", 122 | "destination_sync_mode": "overwrite" 123 | }, 124 | { 125 | "stream": { 126 | "name": "entity_external_link", 127 | "json_schema": {}, 128 | "supported_sync_modes": ["full_refresh"], 129 | "source_defined_cursor": true, 130 | "source_defined_primary_key": [["id"]] 131 | }, 132 | "sync_mode": "full_refresh", 133 | "destination_sync_mode": "overwrite" 134 | }, 135 | { 136 | "stream": { 137 | "name": "project_milestone", 138 | "json_schema": {}, 139 | "supported_sync_modes": ["full_refresh"], 140 | "source_defined_cursor": true, 141 | "source_defined_primary_key": [["id"]] 142 | }, 143 | "sync_mode": "full_refresh", 144 | "destination_sync_mode": "overwrite" 145 | }, 146 | { 147 | "stream": { 148 | "name": "project_update", 149 | "json_schema": {}, 150 | "supported_sync_modes": ["full_refresh"], 151 | "source_defined_cursor": true, 152 | "source_defined_primary_key": [["id"]] 153 | }, 154 | "sync_mode": "full_refresh", 155 | "destination_sync_mode": "overwrite" 156 | }, 157 | { 158 | "stream": { 159 | "name": "team", 160 | "json_schema": {}, 161 | "supported_sync_modes": ["full_refresh"], 162 | "source_defined_cursor": true, 163 | "source_defined_primary_key": [["id"]] 164 | }, 165 | "sync_mode": "full_refresh", 166 | "destination_sync_mode": "overwrite" 167 | }, 168 | { 169 | "stream": { 170 | "name": "team_key", 171 | "json_schema": {}, 172 | "supported_sync_modes": ["full_refresh"], 173 | "source_defined_cursor": true, 174 | "source_defined_primary_key": [["id"]] 175 | }, 176 | "sync_mode": "full_refresh", 177 | "destination_sync_mode": "overwrite" 178 | }, 179 | { 180 | "stream": { 181 | "name": "team_membership", 182 | "json_schema": {}, 183 | "supported_sync_modes": ["full_refresh"], 184 | "source_defined_cursor": true, 185 | "source_defined_primary_key": [["id"]] 186 | }, 187 | "sync_mode": "full_refresh", 188 | "destination_sync_mode": "overwrite" 189 | }, 190 | { 191 | "stream": { 192 | "name": "user", 193 | "json_schema": {}, 194 | "supported_sync_modes": ["full_refresh"], 195 | "source_defined_cursor": true, 196 | "source_defined_primary_key": [["id"]] 197 | }, 198 | "sync_mode": "full_refresh", 199 | "destination_sync_mode": "overwrite" 200 | }, 201 | { 202 | "stream": { 203 | "name": "workflow_state", 204 | "json_schema": {}, 205 | "supported_sync_modes": ["full_refresh"], 206 | "source_defined_cursor": true, 207 | "source_defined_primary_key": [["id"]] 208 | }, 209 | "sync_mode": "full_refresh", 210 | "destination_sync_mode": "overwrite" 211 | }, 212 | { 213 | "stream": { 214 | "name": "project_status", 215 | "json_schema": {}, 216 | "supported_sync_modes": ["full_refresh"], 217 | "source_defined_cursor": true, 218 | "source_defined_primary_key": [["id"]] 219 | }, 220 | "sync_mode": "full_refresh", 221 | "destination_sync_mode": "overwrite" 222 | }, 223 | { 224 | "stream": { 225 | "name": "organization", 226 | "json_schema": {}, 227 | "supported_sync_modes": ["full_refresh"], 228 | "source_defined_cursor": true, 229 | "source_defined_primary_key": [["id"]] 230 | }, 231 | "sync_mode": "full_refresh", 232 | "destination_sync_mode": "overwrite" 233 | }, 234 | { 235 | "stream": { 236 | "name": "cycle", 237 | "json_schema": {}, 238 | "supported_sync_modes": ["full_refresh"], 239 | "source_defined_cursor": true, 240 | "source_defined_primary_key": [["id"]] 241 | }, 242 | "sync_mode": "full_refresh", 243 | "destination_sync_mode": "overwrite" 244 | }, 245 | { 246 | "stream": { 247 | "name": "customer", 248 | "json_schema": {}, 249 | "supported_sync_modes": ["full_refresh"], 250 | "source_defined_cursor": true, 251 | "source_defined_primary_key": [["id"]] 252 | }, 253 | "sync_mode": "full_refresh", 254 | "destination_sync_mode": "overwrite" 255 | }, 256 | { 257 | "stream": { 258 | "name": "customer_need", 259 | "json_schema": {}, 260 | "supported_sync_modes": ["full_refresh"], 261 | "source_defined_cursor": true, 262 | "source_defined_primary_key": [["id"]] 263 | }, 264 | "sync_mode": "full_refresh", 265 | "destination_sync_mode": "overwrite" 266 | } 267 | ] 268 | } 269 | -------------------------------------------------------------------------------- /test_files/invalid_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "invalid" 3 | } --------------------------------------------------------------------------------