diff --git a/.github/workflows/check-oas-updates.yml b/.github/workflows/check-oas-updates.yml new file mode 100644 index 00000000..471f625c --- /dev/null +++ b/.github/workflows/check-oas-updates.yml @@ -0,0 +1,87 @@ +name: Check OAS Updates + +on: + schedule: + # Run at 00:00 UTC on Monday, Wednesday, and Friday + - cron: "0 0 * * 1,3,5" + workflow_dispatch: + # Allow manual triggering + +jobs: + check-oas-updates: + name: Check for OAS updates + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Fetch latest OAS specs + run: bun run fetch:specs + + - name: Check for OAS changes + id: check-oas-changes + run: | + if [[ -n "$(git status --porcelain .oas/)" ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + echo "::notice::OAS changes detected" + else + echo "changes=false" >> $GITHUB_OUTPUT + echo "::notice::No OAS changes detected" + fi + + - name: Verify changes work with the codebase + if: steps.check-oas-changes.outputs.changes == 'true' + run: | + # Run tests to ensure the updated OAS files don't break anything + bun run test + + # Run linter to ensure code quality + bun run lint + + # Verify the package builds correctly with the updated OAS files + bun run build + + - name: Configure Git + if: steps.check-oas-changes.outputs.changes == 'true' + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Create Pull Request + if: steps.check-oas-changes.outputs.changes == 'true' + id: create-pr + uses: peter-evans/create-pull-request@v5 + with: + commit-message: "chore: update OAS specifications" + title: "chore: update OAS specifications" + body: | + This PR updates the OpenAPI specifications to the latest version. + + Changes were automatically detected and committed by the GitHub Actions workflow. + + - [x] OAS files updated + - [x] Verified changes work with the codebase (tests, lint, build) + branch: auto-update-oas + base: main + delete-branch: true + labels: | + automated + dependencies + + - name: PR Summary + if: steps.create-pr.outputs.pull-request-number + run: | + echo "::notice::Created PR #${{ steps.create-pr.outputs.pull-request-number }} with OAS updates" + echo "::notice::PR URL: ${{ steps.create-pr.outputs.pull-request-url }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d08b110..a477b805 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,9 +8,6 @@ on: jobs: test: runs-on: ubuntu-latest - env: - STACKONE_API_KEY: ${{ secrets.STACKONE_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: - uses: actions/checkout@v4 diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc6..2312dc58 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - npx lint-staged diff --git a/.oas/README.md b/.oas/README.md new file mode 100644 index 00000000..b783455d --- /dev/null +++ b/.oas/README.md @@ -0,0 +1,40 @@ +# Generated OpenAPI Specifications + +This directory contains automatically generated OpenAPI specification files for the StackOne API. + +## Important Notes + +- **DO NOT EDIT** these files manually. They are automatically generated by the `fetch:specs` script. +- The files in this directory are updated automatically by the GitHub Actions workflow. +- If you need to update these files, run `bun run fetch:specs` or `bun run rebuild`. + +## File Structure + +Each file represents a different vertical in the StackOne API: + +- `ats.json` - Applicant Tracking Systems +- `core.json` - Core API +- `crm.json` - Customer Relationship Management +- `documents.json` - Document Management +- `hris.json` - Human Resources Information Systems +- `iam.json` - Identity and Access Management +- `lms.json` - Learning Management Systems +- `marketing.json` - Marketing Automation + +## Adding a new specification + +These specs are pulled from the [StackOne Docs](https://docs.stackone.com/openapi). + +To add a new specification, run the following command: + +```bash +bun run fetch:specs +``` + +This will download the latest specification and save it in the `.oas` directory. + +You may need to update snapshot tests after fetching new specs. + +```bash +bun test +``` diff --git a/src/oas/ats.json b/.oas/ats.json similarity index 100% rename from src/oas/ats.json rename to .oas/ats.json diff --git a/src/oas/core.json b/.oas/core.json similarity index 100% rename from src/oas/core.json rename to .oas/core.json diff --git a/src/oas/crm.json b/.oas/crm.json similarity index 100% rename from src/oas/crm.json rename to .oas/crm.json diff --git a/src/oas/documents.json b/.oas/documents.json similarity index 100% rename from src/oas/documents.json rename to .oas/documents.json diff --git a/src/oas/hris.json b/.oas/hris.json similarity index 96% rename from src/oas/hris.json rename to .oas/hris.json index d4e05a36..c016244e 100644 --- a/src/oas/hris.json +++ b/.oas/hris.json @@ -775,7 +775,7 @@ "description": "The comma separated list of fields that will be expanded in the response", "schema": { "nullable": true, - "example": "company,employments,work_location,home_location,groups", + "example": "company,employments,work_location,home_location,groups,skills", "type": "string" } }, @@ -1005,7 +1005,7 @@ "description": "The comma separated list of fields that will be expanded in the response", "schema": { "nullable": true, - "example": "company,employments,work_location,home_location,groups", + "example": "company,employments,work_location,home_location,groups,skills", "type": "string" } }, @@ -7353,6 +7353,195 @@ } }, "/unified/hris/employees/{id}/skills": { + "get": { + "operationId": "hris_list_employee_skills", + "parameters": [ + { + "name": "x-account-id", + "in": "header", + "description": "The account identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "raw", + "required": false, + "in": "query", + "description": "Indicates that the raw request result is returned", + "schema": { + "nullable": true, + "default": false, + "type": "boolean" + } + }, + { + "name": "proxy", + "required": false, + "in": "query", + "description": "Query parameters that can be used to pass through parameters to the underlying provider request by surrounding them with 'proxy' key", + "style": "deepObject", + "explode": true, + "schema": { + "additionalProperties": true, + "nullable": true, + "type": "object" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", + "schema": { + "nullable": true, + "example": "id,remote_id,name,active,language,maximum_proficiency,minimum_proficiency", + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "Filter parameters that allow greater customisation of the list response", + "explode": true, + "style": "deepObject", + "schema": { + "properties": { + "updated_after": { + "description": "Use a string with a date to only select results updated after that given date", + "example": "2020-01-01T00:00:00.000Z", + "type": "string", + "nullable": true, + "additionalProperties": false + } + }, + "nullable": true, + "type": "object" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "description": "The page number of the results to fetch", + "deprecated": true, + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "name": "page_size", + "required": false, + "in": "query", + "description": "The number of results per page", + "schema": { + "nullable": true, + "default": "25", + "type": "string" + } + }, + { + "name": "next", + "required": false, + "in": "query", + "description": "The unified cursor", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "name": "updated_after", + "required": false, + "in": "query", + "description": "Use a string with a date to only select results updated after that given date", + "deprecated": true, + "schema": { + "nullable": true, + "example": "2020-01-01T00:00:00.000Z", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The skills related to the employee with the given identifier were retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntitySkillsPaginated" + } + } + } + }, + "400": { + "description": "Invalid request." + }, + "403": { + "description": "Forbidden." + }, + "408": { + "description": "The request has timed out.", + "headers": { + "Retry-After": { + "description": "A time in seconds after which the request can be retried.", + "schema": { + "type": "string" + } + } + } + }, + "412": { + "description": "Precondition failed: linked account belongs to a disabled integration." + }, + "429": { + "description": "Too many requests." + }, + "500": { + "description": "Server error while executing the request." + }, + "501": { + "description": "This functionality is not implemented." + } + }, + "security": [ + { + "basic": [] + } + ], + "summary": "List Employee Skills", + "tags": ["Employees"], + "x-speakeasy-group": "hris", + "x-speakeasy-name-override": "list_employee_skills", + "x-speakeasy-pagination": { + "type": "cursor", + "inputs": [ + { + "name": "next", + "in": "parameters", + "type": "cursor" + } + ], + "outputs": { + "nextCursor": "$.next" + } + }, + "x-speakeasy-retries": { + "statusCodes": [429, 408], + "strategy": "backoff" + } + }, "post": { "operationId": "hris_create_employee_skill", "parameters": [ @@ -7440,6 +7629,127 @@ } } }, + "/unified/hris/employees/{id}/skills/{subResourceId}": { + "get": { + "operationId": "hris_get_employee_skill", + "parameters": [ + { + "name": "x-account-id", + "in": "header", + "description": "The account identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "subResourceId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "raw", + "required": false, + "in": "query", + "description": "Indicates that the raw request result is returned", + "schema": { + "nullable": true, + "default": false, + "type": "boolean" + } + }, + { + "name": "proxy", + "required": false, + "in": "query", + "description": "Query parameters that can be used to pass through parameters to the underlying provider request by surrounding them with 'proxy' key", + "style": "deepObject", + "explode": true, + "schema": { + "additionalProperties": true, + "nullable": true, + "type": "object" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", + "schema": { + "nullable": true, + "example": "id,remote_id,name,active,language,maximum_proficiency,minimum_proficiency", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The skill related to the employee with the given identifiers was retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntitySkillResult" + } + } + } + }, + "400": { + "description": "Invalid request." + }, + "403": { + "description": "Forbidden." + }, + "408": { + "description": "The request has timed out.", + "headers": { + "Retry-After": { + "description": "A time in seconds after which the request can be retried.", + "schema": { + "type": "string" + } + } + } + }, + "412": { + "description": "Precondition failed: linked account belongs to a disabled integration." + }, + "429": { + "description": "Too many requests." + }, + "500": { + "description": "Server error while executing the request." + }, + "501": { + "description": "This functionality is not implemented." + } + }, + "security": [ + { + "basic": [] + } + ], + "summary": "Get Employee Skill", + "tags": ["Employees"], + "x-speakeasy-group": "hris", + "x-speakeasy-name-override": "get_employee_skill", + "x-speakeasy-retries": { + "statusCodes": [429, 408], + "strategy": "backoff" + } + } + }, "/unified/hris/time_off_policies": { "get": { "operationId": "hris_list_time_off_policies", @@ -9837,6 +10147,78 @@ } } }, + "EntitySkillResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/EntitySkills" + }, + "raw": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/RawResponse" + } + } + }, + "required": ["data"] + }, + "EntitySkills": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID associated with this skill", + "example": "16873-IT345", + "nullable": true + }, + "remote_id": { + "type": "string", + "description": "Provider's unique identifier", + "example": "8187e5da-dc77-475e-9949-af0f1fa4e4e3", + "nullable": true + }, + "name": { + "type": "string", + "description": "The name associated with this skill", + "example": "Information-Technology", + "nullable": true + }, + "active": { + "type": "boolean", + "description": "Whether the skill is active and therefore available for use", + "example": true, + "nullable": true + }, + "language": { + "description": "The language associated with this skill", + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LanguageEnum" + } + ] + }, + "maximum_proficiency": { + "description": "The proficiency level of the skill", + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Proficiency" + } + ] + }, + "minimum_proficiency": { + "description": "The proficiency level of the skill", + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Proficiency" + } + ] + } + } + }, "EntitySkillsCreateRequestDto": { "type": "object", "properties": { @@ -9872,6 +10254,29 @@ } } }, + "EntitySkillsPaginated": { + "type": "object", + "properties": { + "next": { + "type": "string", + "nullable": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntitySkills" + } + }, + "raw": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/RawResponse" + } + } + }, + "required": ["data"] + }, "EthnicityEnum": { "type": "object", "properties": { @@ -17384,6 +17789,452 @@ } } }, + "LanguageEnum": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": [ + "ar_AR", + "aa_ER", + "af_NA", + "af_ZA", + "am_ET", + "ar_AE", + "ar_BH", + "ar_DJ", + "ar_DZ", + "ar_EG", + "ar_ER", + "ar_IQ", + "ar_JO", + "ar_KM", + "ar_KW", + "ar_LB", + "ar_LY", + "ar_MA", + "ar_MR", + "ar_OM", + "ar_PS", + "ar_QA", + "ar_SA", + "ar_SD", + "ar_SY", + "ar_TD", + "ar_TN", + "ar_YE", + "ay_BO", + "ay_PE", + "az_AZ", + "az_IR", + "be_BY", + "bg_BG", + "bi_VU", + "bn_BD", + "bn_IN", + "bs_BA", + "bs-ME", + "byn_ER", + "ca_AD", + "ca_ES", + "ca_FR", + "ca_IT", + "ch_GU", + "cs_CZ", + "da_DK", + "de_AT", + "de_BE", + "de_CH", + "de_DE", + "de_LI", + "de_LU", + "de_VA", + "de_MV", + "dv_MV", + "dz_BT", + "el_CY", + "el_GR", + "en_AG", + "en_AI", + "en_AS", + "en_AU", + "en_BB", + "en_BE", + "en_BM", + "en_BS", + "en_BW", + "en_BZ", + "en_CA", + "en_CC", + "en_CK", + "en_CM", + "en_CW", + "en_CX", + "en_DG", + "en_DM", + "en_ER", + "en_FJ", + "en_FK", + "en_FM", + "en_GB", + "en_GD", + "en_GG", + "en_GH", + "en_GI", + "en_GM", + "en_GS", + "en_GU", + "en_GY", + "en_HK", + "en_IE", + "en_IM", + "en_IN", + "en_IO", + "en_JE", + "en_JM", + "en_KE", + "en_KI", + "en_KN", + "en_KY", + "en_LC", + "en_LR", + "en_LS", + "en_MF", + "en_MG", + "en_MH", + "en_MO", + "en_MP", + "en_MS", + "en_MT", + "en_MU", + "en_MW", + "en_MY", + "en_NA", + "en_NF", + "en_NG", + "en_NL", + "en_NR", + "en_NU", + "en_NZ", + "en_PG", + "en_PH", + "en_PK", + "en_PN", + "en_PR", + "en_PW", + "en_RW", + "en_SB", + "en_SC", + "en_SD", + "en_SG", + "en_SH", + "en_SL", + "en_SS", + "en_SX", + "en_SZ", + "en_TC", + "en_TK", + "en_TO", + "en_TT", + "en_TV", + "en_TZ", + "en_UG", + "en_UM", + "en_US", + "en_VC", + "en_VG", + "en_VI", + "en_VU", + "en_WS", + "en_ZA", + "en_ZM", + "en_ZW", + "es_AR", + "es_BO", + "es_BZ", + "es_CL", + "es_CO", + "es_CR", + "es_CU", + "es_DO", + "es_EA", + "es_EC", + "es_EH", + "es_ES", + "es_GQ", + "es_GT", + "es_HN", + "es_IC", + "es_LA", + "es_MX", + "es_NI", + "es_PA", + "es_PE", + "es_PH", + "es_PR", + "es_PY", + "es_SV", + "es_US", + "es_UY", + "es_VE", + "et_EE", + "fa_AF", + "fa_IR", + "fan_GA", + "ff_CM", + "ff_GN", + "ff_MR", + "ff_SN", + "ff_BF", + "fi_FI", + "fj_FJ", + "fo_FO", + "fr_BE", + "fr_BF", + "fr_BI", + "fr_BJ", + "fr_BL", + "fr_CA", + "fr_CD", + "fr_CF", + "fr_CG", + "fr_CH", + "fr_CI", + "fr_CM", + "fr_DJ", + "fr_DZ", + "fr_FR", + "fr_GA", + "fr_GF", + "fr_GG", + "fr_GN", + "fr_GP", + "fr_GQ", + "fr_HT", + "fr_KM", + "fr_JE", + "fr_LU", + "fr_LB", + "fr_MA", + "fr_MC", + "fr_MF", + "fr_MG", + "fr_ML", + "fr_MQ", + "fr_MR", + "fr_MU", + "fr_NC", + "fr_NE", + "fr_PF", + "fr_PM", + "fr_RE", + "fr_RW", + "fr_SC", + "fr_SN", + "fr_SY", + "fr_TD", + "fr_TF", + "fr_TG", + "fr_TN", + "fr_VU", + "fr_VA", + "fr_WF", + "fr_YT", + "ga_IE", + "gn_PY", + "gn_AR", + "gu_IN", + "gv_IM", + "he_IL", + "hi_IN", + "hr_BA", + "hr_HR", + "hr_ME", + "ht_HT", + "hu_HU", + "hy_AM", + "hy_CY", + "id_ID", + "is_IS", + "it_CH", + "it_IT", + "it_SM", + "it_VA", + "ja_JP", + "ka_GE", + "kg_CD", + "kk_KZ", + "kl_GL", + "km_KH", + "ko_KP", + "ko_KR", + "ku_IQ", + "ky_KG", + "la_VA", + "lb_LU", + "ln_AO", + "ln_CD", + "ln_CF", + "ln_CG", + "lo_LA", + "lt_LT", + "lu_CD", + "lv_LV", + "mg_MG", + "mh_MH", + "mi_NZ", + "mk_MK", + "mn_MN", + "mr_IN", + "ms_BN", + "ms_MY", + "ms_SG", + "mt_MT", + "my_MM", + "nb_NO", + "nb_BV", + "nb_ZW", + "ne_NP", + "nl_AW", + "nl_BE", + "nl_BQ", + "nl_CW", + "nl_NL", + "nl_SR", + "nl_SX", + "nl_MF", + "nn_NO", + "nn_BV", + "no_NO", + "no_BV", + "no_SJ", + "nr_ZA", + "ny_MW", + "pa_IN", + "pa_PK", + "pl_PL", + "ps_AF", + "pt_AO", + "pt_BR", + "pt_CH", + "pt_CV", + "pt_GQ", + "pt_GW", + "pt_LU", + "pt_MO", + "pt_MZ", + "pt_PT", + "pt_ST", + "pt_TL", + "qu_BO", + "qu_EC", + "qu_PE", + "rar_CK", + "rm_CH", + "rup_MK", + "ro_MD", + "ro_RO", + "ru_BY", + "ru_KG", + "ru_KZ", + "ru_MD", + "ru_RU", + "ru_UA", + "ru_AQ", + "ru_TJ", + "ru_TM", + "ru_UZ", + "rw_RW", + "se_SE", + "sg_CF", + "si_LK", + "sk_SK", + "sl_SI", + "sm_AS", + "sm_WS", + "sn_ZW", + "so_DJ", + "so_ET", + "so_KE", + "so_SO", + "sq_AL", + "sq_ME", + "sq_XK", + "sr_BA", + "sr_ME", + "sr_RS", + "sr_XK", + "ss_SZ", + "ss_ZA", + "sv_AX", + "sv_FI", + "sv_SE", + "sw_KE", + "sw_TZ", + "sw_UG", + "sw_CD", + "ta_IN", + "ta_MY", + "ta_SG", + "ta_LK", + "te_IN", + "tg_TJ", + "th_TH", + "ti_ER", + "ti_ET", + "tig_ER", + "tk_TM", + "tk_AF", + "tn_BW", + "tn_ZA", + "to_TO", + "tr_CY", + "tr_TR", + "ts_ZA", + "uk_UA", + "ur_IN", + "ur_PK", + "uz_AF", + "uz_UZ", + "ve_ZA", + "vi_VN", + "xh_ZA", + "zh_CN", + "zh_HK", + "zh_MO", + "zh_SG", + "zh_TW", + "zu_ZA", + null + ], + "description": "The Locale Code of the language", + "example": "en_GB", + "x-speakeasy-unknown-values": "allow", + "nullable": true + }, + "source_value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array", + "items": {} + } + ], + "nullable": true + } + } + }, "LocationTypeEnum": { "type": "object", "properties": { diff --git a/src/oas/iam.json b/.oas/iam.json similarity index 100% rename from src/oas/iam.json rename to .oas/iam.json diff --git a/src/oas/lms.json b/.oas/lms.json similarity index 97% rename from src/oas/lms.json rename to .oas/lms.json index d6f8948f..cd28c766 100644 --- a/src/oas/lms.json +++ b/.oas/lms.json @@ -998,7 +998,7 @@ "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", "schema": { "nullable": true, - "example": "id,remote_id,external_reference,course_ids,remote_course_ids,title,description,additional_data,languages,content_url,mobile_launch_content_url,content_type,cover_url,active,duration,order,categories,skills,updated_at,created_at,provider", + "example": "id,remote_id,external_reference,course_ids,remote_course_ids,title,description,additional_data,languages,content_url,mobile_launch_content_url,content_type,cover_url,active,duration,order,categories,skills,updated_at,created_at,provider,localisations,tags", "type": "string" } }, @@ -1265,7 +1265,7 @@ "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", "schema": { "nullable": true, - "example": "id,remote_id,external_reference,course_ids,remote_course_ids,title,description,additional_data,languages,content_url,mobile_launch_content_url,content_type,cover_url,active,duration,order,categories,skills,updated_at,created_at,provider", + "example": "id,remote_id,external_reference,course_ids,remote_course_ids,title,description,additional_data,languages,content_url,mobile_launch_content_url,content_type,cover_url,active,duration,order,categories,skills,updated_at,created_at,provider,localisations,tags", "type": "string" } } @@ -2740,7 +2740,7 @@ "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", "schema": { "nullable": true, - "example": "id,remote_id,name,active,level,language,hierarchy,proficiency", + "example": "id,remote_id,name,active,hierarchy,language", "type": "string" } } @@ -2845,7 +2845,7 @@ "description": "The comma separated list of fields that will be returned in the response (if empty, all fields are returned)", "schema": { "nullable": true, - "example": "id,remote_id,name,active,level,language,hierarchy,proficiency", + "example": "id,remote_id,name,active,hierarchy,language", "type": "string" } }, @@ -4273,6 +4273,47 @@ "example": "This course is a valuable resource and acts as learning content for...", "deprecated": true, "nullable": true + }, + "localisations": { + "description": "Localised content information", + "example": [ + { + "title": "Software Engineer Lv 1", + "description": "This video acts as learning content for software engineers.", + "languages": { + "value": "en-US", + "source_value": "string" + } + } + ], + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalisationModel" + } + }, + "tags": { + "description": "A list of tags associated with the content", + "example": ["Sales Techniques", "Customer Service"], + "nullable": true, + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string", + "description": "The date on which the content was last updated.", + "example": "2021-07-21T14:00:00.000Z", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "description": "The date on which the content was created.", + "example": "2021-07-21T14:00:00.000Z", + "format": "date-time", + "nullable": true } } }, @@ -4325,7 +4366,7 @@ "properties": { "value": { "type": "string", - "enum": ["video", "quiz", "document", null], + "enum": ["video", "quiz", "document", "audio", "article", null], "x-speakeasy-unknown-values": "allow", "nullable": true }, @@ -4624,6 +4665,12 @@ "example": "https://www.youtube.com/watch?v=16873", "nullable": true }, + "mobile_launch_content_url": { + "type": "string", + "description": "The mobile friendly URL of the content", + "example": "https://www.mobile.youtube.com/watch?v=16873", + "nullable": true + }, "order": { "type": "number", "description": "The order of the individual content within a content grouping. This is not applicable for pushing individual content.", @@ -5565,6 +5612,47 @@ "deprecated": true, "nullable": true }, + "localisations": { + "description": "Localised content information", + "example": [ + { + "title": "Software Engineer Lv 1", + "description": "This video acts as learning content for software engineers.", + "languages": { + "value": "en-US", + "source_value": "string" + } + } + ], + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalisationModel" + } + }, + "tags": { + "description": "A list of tags associated with the content", + "example": ["Sales Techniques", "Customer Service"], + "nullable": true, + "type": "array", + "items": { + "type": "string" + } + }, + "updated_at": { + "type": "string", + "description": "The date on which the content was last updated.", + "example": "2021-07-21T14:00:00.000Z", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "description": "The date on which the content was created.", + "example": "2021-07-21T14:00:00.000Z", + "format": "date-time", + "nullable": true + }, "categories": { "description": "The categories associated with this content", "nullable": true, @@ -5736,6 +5824,32 @@ } } }, + "LocalisationModel": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the content", + "example": "Software Engineer Lv 1", + "nullable": true + }, + "description": { + "type": "string", + "description": "The description of the content", + "example": "This video acts as learning content for software engineers.", + "nullable": true + }, + "language": { + "description": "The language associated with this localisation details", + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LanguageEnum" + } + ] + } + } + }, "RawResponse": { "type": "object", "properties": { diff --git a/src/oas/marketing.json b/.oas/marketing.json similarity index 100% rename from src/oas/marketing.json rename to .oas/marketing.json diff --git a/README.md b/README.md index 6c6f8d4a..57ce63f0 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ import { StackOneToolSet } from "@stackone/ai"; // Initialize with a custom base URL const toolset = new StackOneToolSet( - process.env.STACKONE_API_KEY, - process.env.STACKONE_ACCOUNT_ID, + "your-api-key", + "your-account-id", "https://api.example-dev.com" ); @@ -77,7 +77,7 @@ if (employeeTool) { } ``` -## AI Framework Integrations +## Integrations ### OpenAI @@ -97,7 +97,7 @@ const openai = new OpenAI({ }); const response = await openai.chat.completions.create({ - model: "gpt-4-turbo", + model: "gpt-4o-mini", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "List all employees" }, @@ -106,6 +106,28 @@ const response = await openai.chat.completions.create({ }); ``` +### AI SDK + +```typescript +import { StackOneToolSet } from "@stackone/ai"; +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +const toolset = new StackOneToolSet(); +const tools = toolset.getTools("hris_*", "your-account-id"); + +// Convert to AI SDK tools +const aiSdkTools = tools.toAISDKTools(); +// Use max steps to automatically call the tool if it's needed +const { text } = await generateText({ + model: openai("gpt-4o-mini"), + tools: aiSdkTools, + prompt: + "Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA", + maxSteps: 3, +}); +``` + ## Error Handling The SDK provides specific error classes for different types of errors: diff --git a/biome.json b/biome.json index 75a729e2..01afb654 100644 --- a/biome.json +++ b/biome.json @@ -37,6 +37,7 @@ } }, "files": { - "ignoreUnknown": true + "ignoreUnknown": true, + "include": ["src/**/*.ts"] } } diff --git a/bun.lock b/bun.lock index b659849f..a26a76be 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "lint-staged": "^15.2.0", "mkdocs": "^0.0.1", "openapi-types": "^12.1.3", + "rimraf": "^6.0.1", "typescript": "^5.0.0", }, "peerDependencies": { @@ -56,6 +57,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], @@ -84,6 +87,10 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], @@ -96,6 +103,10 @@ "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -116,6 +127,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -138,6 +151,8 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], @@ -154,6 +169,8 @@ "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + "glob": ["glob@11.0.1", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -176,6 +193,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jackspeak": ["jackspeak@4.1.0", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], @@ -188,6 +207,8 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -202,6 +223,10 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mkdocs": ["mkdocs@0.0.1", "", {}, "sha512-7AtZyiwELtQQcP2Cdw0H8Q413alLa4Jl0AnDIreqP8+5GJvLFLMoh4gFQ21i6ASdKNlKGPjTHqiVDBg/lQ0dQg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -220,8 +245,12 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], @@ -232,6 +261,8 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@6.0.1", "", { "dependencies": { "glob": "^11.0.0", "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -246,8 +277,12 @@ "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], "swr": ["swr@2.3.2", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA=="], @@ -274,12 +309,18 @@ "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -288,8 +329,32 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index 69f217fc..b5cab59f 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,12 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", - "files": ["dist", "README.md", "LICENSE"], + "files": ["dist", ".oas", "README.md", "LICENSE"], "scripts": { + "prepare": "husky && bun run fetch:specs", + "prebuild": "rimraf dist", "build": "bun build ./src/index.ts --outdir ./dist --target node", + "rebuild": "bun run fetch:specs && bun run build", "publish-package": "bun run build && bun publish --access=public", "test": "bun test", "fetch:specs": "bun run ./scripts/fetch-specs.ts", @@ -31,6 +34,7 @@ "lint-staged": "^15.2.0", "mkdocs": "^0.0.1", "openapi-types": "^12.1.3", + "rimraf": "^6.0.1", "typescript": "^5.0.0" }, "dependencies": { diff --git a/scripts/fetch-specs.ts b/scripts/fetch-specs.ts index 0a89b0ee..c4dd28e3 100755 --- a/scripts/fetch-specs.ts +++ b/scripts/fetch-specs.ts @@ -3,21 +3,16 @@ * Script to fetch OpenAPI specifications from the StackOne documentation * * This script scrapes the StackOne documentation page to find all available - * OpenAPI specifications, then downloads and saves them to the src/openapi directory. + * OpenAPI specifications, then downloads and saves them to the .oas directory. */ - import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { config } from 'dotenv'; - -// Load environment variables -config(); // Configuration const STACKONE_DOCS_BASE = 'https://docs.stackone.com'; const STACKONE_DOCS_URL = `${STACKONE_DOCS_BASE}/openapi`; -const OUTPUT_DIR = join(process.cwd(), 'src', 'oas'); +const OUTPUT_DIR = join(process.cwd(), '.oas'); /** * Scrape OpenAPI spec URLs and their IDs from the documentation page diff --git a/src/constants.ts b/src/constants.ts index 3c23e931..f21532e3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -5,5 +6,27 @@ import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Use bundled specs directly -export const OAS_DIR = path.join(__dirname, 'oas'); +// OAS files are in the top-level .oas directory +// This ensures consistency between local development and the published package +const determineOasDir = (): string => { + // First, try to find the .oas directory relative to the module's parent directory + // This handles both development and when used as a dependency + const projectRoot = path.resolve(__dirname, '..'); + const oasDir = path.join(projectRoot, '.oas'); + + if (fs.existsSync(oasDir)) { + return oasDir; + } + + // If not found, try to find it relative to the current working directory + // This is a fallback for unusual project structures + const cwdOasDir = path.join(process.cwd(), '.oas'); + if (fs.existsSync(cwdOasDir)) { + return cwdOasDir; + } + + // Default to the project root path, even if it doesn't exist yet + return oasDir; +}; + +export const OAS_DIR = determineOasDir(); diff --git a/src/openapi/parser.ts b/src/openapi/parser.ts index 13af16be..076b9e24 100644 --- a/src/openapi/parser.ts +++ b/src/openapi/parser.ts @@ -11,8 +11,17 @@ interface ExtendedJsonSchema extends Omit { type?: ExtendedSchemaType | ExtendedSchemaType[]; } +// Define a type for OpenAPI document +type OpenAPIDocument = OpenAPIV3.Document | OpenAPIV3_1.Document; + +// Define a type for schema objects +type SchemaObject = OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; + +// Define HTTP methods type +type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace'; + export class OpenAPIParser { - private spec: OpenAPIV3.Document | OpenAPIV3_1.Document; + private spec: OpenAPIDocument; private baseUrl: string; /** @@ -20,14 +29,11 @@ export class OpenAPIParser { * @param specPathOrObject Path to the OpenAPI specification file or the spec object directly * @param customBaseUrl Optional custom base URL to override the one in the spec */ - constructor( - specPathOrObject: string | OpenAPIV3.Document | OpenAPIV3_1.Document, - customBaseUrl?: string - ) { + constructor(specPathOrObject: string | OpenAPIDocument, customBaseUrl?: string) { // Initialize spec either from file or directly from object if (typeof specPathOrObject === 'string') { const specContent = fs.readFileSync(specPathOrObject, 'utf-8'); - this.spec = JSON.parse(specContent) as OpenAPIV3.Document | OpenAPIV3_1.Document; + this.spec = JSON.parse(specContent) as OpenAPIDocument; } else { this.spec = specPathOrObject; } @@ -50,7 +56,7 @@ export class OpenAPIParser { * @returns A new OpenAPIParser instance */ static fromString(jsonString: string, customBaseUrl?: string): OpenAPIParser { - const spec = JSON.parse(jsonString) as OpenAPIV3.Document | OpenAPIV3_1.Document; + const spec = JSON.parse(jsonString) as OpenAPIDocument; return new OpenAPIParser(spec, customBaseUrl); } @@ -106,33 +112,39 @@ export class OpenAPIParser { visited.add(ref); const parts = ref.split('/').slice(1); // Skip the '#' - let current: any = this.spec; + let current: unknown = this.spec; for (const part of parts) { - current = current[part]; + if (typeof current === 'object' && current !== null) { + current = (current as Record)[part]; + } else { + throw new Error(`Invalid reference path: ${ref}`); + } } // After getting the referenced schema, resolve it fully - return this._resolveSchema(current, visited); + return this._resolveSchema(current as SchemaObject, visited); } /** * Resolve all references in a schema, preserving structure */ private _resolveSchema( - schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | any, + schema: SchemaObject | unknown, visited: Set = new Set() ): JsonSchema { // Handle primitive types (string, number, etc) if (typeof schema !== 'object' || schema === null) { - return schema as any; + return schema as JsonSchema; } if (Array.isArray(schema)) { - return schema.map((item) => this._resolveSchema(item, new Set(visited))) as any; + return schema.map((item) => + this._resolveSchema(item, new Set(visited)) + ) as unknown as JsonSchema; } // Handle direct reference - if ('$ref' in schema) { + if (typeof schema === 'object' && '$ref' in schema && typeof schema.$ref === 'string') { const resolved = this._resolveSchemaRef(schema.$ref, visited); if (typeof resolved !== 'object' || resolved === null) { return resolved; @@ -145,32 +157,36 @@ export class OpenAPIParser { } // Handle allOf combinations - if ('allOf' in schema) { - const mergedSchema: JsonSchema = { ...schema }; - (mergedSchema as any).allOf = undefined; + if (typeof schema === 'object' && 'allOf' in schema) { + const schemaObj = schema as OpenAPIV3.SchemaObject; + // Create a new object without the allOf property to avoid type issues + const { allOf, ...restSchema } = schemaObj; + const mergedSchema: JsonSchema = restSchema as JsonSchema; // Merge all schemas in allOf array - for (const subSchema of schema.allOf || []) { - const resolved = this._resolveSchema(subSchema, new Set(visited)); - if (typeof resolved !== 'object' || resolved === null) { - continue; - } + if (Array.isArray(allOf)) { + for (const subSchema of allOf) { + const resolved = this._resolveSchema(subSchema, new Set(visited)); + if (typeof resolved !== 'object' || resolved === null) { + continue; + } - // Merge properties - if ('properties' in resolved) { - if (!mergedSchema.properties) { - mergedSchema.properties = {}; + // Merge properties + if ('properties' in resolved) { + if (!mergedSchema.properties) { + mergedSchema.properties = {}; + } + mergedSchema.properties = { + ...mergedSchema.properties, + ...resolved.properties, + }; } - mergedSchema.properties = { - ...mergedSchema.properties, - ...resolved.properties, - }; - } - // Merge type and other fields - for (const [key, value] of Object.entries(resolved)) { - if (key !== 'properties' && !(key in mergedSchema)) { - (mergedSchema as any)[key] = value; + // Merge type and other fields + for (const [key, value] of Object.entries(resolved)) { + if (key !== 'properties' && !(key in mergedSchema)) { + (mergedSchema as Record)[key] = value; + } } } } @@ -179,20 +195,20 @@ export class OpenAPIParser { } // Recursively resolve all nested objects and arrays - const resolved: JsonSchema = {}; + const resolved: Record = {}; for (const [key, value] of Object.entries(schema)) { if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { - (resolved as any)[key] = value.map((item) => this._resolveSchema(item, new Set(visited))); + resolved[key] = value.map((item) => this._resolveSchema(item, new Set(visited))); } else { - (resolved as any)[key] = this._resolveSchema(value, new Set(visited)); + resolved[key] = this._resolveSchema(value, new Set(visited)); } } else { - (resolved as any)[key] = value; + resolved[key] = value; } } - return resolved; + return resolved as JsonSchema; } /** @@ -237,9 +253,13 @@ export class OpenAPIParser { if ('$ref' in requestBody) { const ref = requestBody.$ref as string; const parts = ref.split('/').slice(1); - let current: any = this.spec; + let current: unknown = this.spec; for (const part of parts) { - current = current[part]; + if (typeof current === 'object' && current !== null) { + current = (current as Record)[part]; + } else { + throw new Error(`Invalid reference path: ${ref}`); + } } resolvedRequestBody = current as OpenAPIV3.RequestBodyObject; } else { @@ -330,7 +350,7 @@ export class OpenAPIParser { // Add to properties for tool parameters const schema = { ...(resolvedParam.schema || {}) }; if ('description' in resolvedParam) { - (schema as any).description = resolvedParam.description; + (schema as Record).description = resolvedParam.description; } properties[paramName] = this._resolveSchema(schema); } @@ -352,7 +372,7 @@ export class OpenAPIParser { properties, }, execute: { - method: method.toUpperCase() as any, + method: method.toUpperCase(), url: `${this.baseUrl}${path}`, name, headers: {}, @@ -375,9 +395,20 @@ export class OpenAPIParser { const operations: [string, OpenAPIV3.OperationObject][] = []; // Handle HTTP methods - const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; + const methods: HttpMethod[] = [ + 'get', + 'put', + 'post', + 'delete', + 'options', + 'head', + 'patch', + 'trace', + ]; for (const method of methods) { - const operation = (pathItem as any)[method] as OpenAPIV3.OperationObject | undefined; + const operation = (pathItem as Record)[method] as + | OpenAPIV3.OperationObject + | undefined; if (operation) { operations.push([method, operation]); } @@ -395,9 +426,13 @@ export class OpenAPIParser { if ('$ref' in param) { const ref = param.$ref as string; const parts = ref.split('/').slice(1); - let current: any = this.spec; + let current: unknown = this.spec; for (const part of parts) { - current = current[part]; + if (typeof current === 'object' && current !== null) { + current = (current as Record)[part]; + } else { + throw new Error(`Invalid reference path: ${ref}`); + } } return current as OpenAPIV3.ParameterObject; } diff --git a/src/tests/fetch-specs.spec.ts b/src/tests/fetch-specs.spec.ts index 67b97ff8..ecfeefd2 100644 --- a/src/tests/fetch-specs.spec.ts +++ b/src/tests/fetch-specs.spec.ts @@ -1,6 +1,5 @@ import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test'; import fs from 'node:fs'; -import path from 'node:path'; // Mock the fetch function with the correct signature const mockFetch = mock((input: URL | RequestInfo, _init?: RequestInit) => { @@ -76,8 +75,14 @@ const mockFetch = mock((input: URL | RequestInfo, _init?: RequestInit) => { // Mock environment variables Bun.env.STACKONE_API_KEY = 'test_api_key'; -// Create a temporary output directory for testing -const TEST_OUTPUT_DIR = path.join(process.cwd(), 'tests', 'tmp', 'oas'); +// Mock fs module +const mockWriteFileSync = mock((_: string, __: string) => { + // Do nothing, just track that it was called + return undefined; +}); + +// Store the original fs.writeFileSync +const originalWriteFileSync = fs.writeFileSync; describe('fetch-specs script', () => { // Save original functions @@ -86,21 +91,13 @@ describe('fetch-specs script', () => { beforeAll(() => { // Replace functions with mocks globalThis.fetch = mockFetch as typeof fetch; - - // Create test output directory - if (!fs.existsSync(TEST_OUTPUT_DIR)) { - fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true }); - } + fs.writeFileSync = mockWriteFileSync as typeof fs.writeFileSync; }); afterAll(() => { // Restore original functions globalThis.fetch = originalFetch; - - // Clean up test directory - if (fs.existsSync(TEST_OUTPUT_DIR)) { - fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); - } + fs.writeFileSync = originalWriteFileSync; }); it('should fetch and save OpenAPI specs', async () => { @@ -138,25 +135,20 @@ describe('fetch-specs script', () => { expect((error as Error).message).toContain('Failed to fetch'); } - // Since we can't properly mock the fs/promises functions in Bun yet, - // we'll just test that the saveSpec function can be called without errors + // Test saveSpec function using mocked fs.writeFileSync const saveSpec = async (category: string, spec: Record): Promise => { - // Just a simple implementation that doesn't use the mocked functions - const outputPath = path.join(TEST_OUTPUT_DIR, `${category}.json`); - // Write to the file directly instead of using the mocked functions + // Use a mock path that doesn't need to be created + const outputPath = `/mock/path/${category}.json`; fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2)); }; // Test saveSpec function await saveSpec('hris', hrisSpec); - // Verify the file was created - const outputPath = path.join(TEST_OUTPUT_DIR, 'hris.json'); - expect(fs.existsSync(outputPath)).toBe(true); + // Verify that writeFileSync was called + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); - // Clean up - if (fs.existsSync(outputPath)) { - fs.unlinkSync(outputPath); - } + // Reset mock call count + mockWriteFileSync.mockClear(); }); }); diff --git a/src/tests/openapi-parser.spec.ts b/src/tests/openapi-parser.spec.ts index 88891e84..bf8fddd6 100644 --- a/src/tests/openapi-parser.spec.ts +++ b/src/tests/openapi-parser.spec.ts @@ -3,6 +3,9 @@ import type { OpenAPIV3 } from 'openapi-types'; import { ParameterLocation } from '../models'; import { OpenAPIParser } from '../openapi/parser'; +// Define a type for customization options +type SpecCustomization = Partial; + // Mock OpenAPI specs for testing const mockCoreSpec: OpenAPIV3.Document = { openapi: '3.0.0', @@ -43,7 +46,7 @@ const mockCoreSpec: OpenAPIV3.Document = { }; // Helper function to create a minimal OpenAPI spec for testing specific functionality -const createMinimalSpec = (customization: any = {}): OpenAPIV3.Document => { +const createMinimalSpec = (customization: SpecCustomization = {}): OpenAPIV3.Document => { return { openapi: '3.0.0', info: { @@ -55,6 +58,42 @@ const createMinimalSpec = (customization: any = {}): OpenAPIV3.Document => { }; }; +// Define specific function types for private methods +type IsFileTypeFunction = (schema: Record) => boolean; +type ConvertToFileTypeFunction = (schema: Record) => void; +type HandleFilePropertiesFunction = (schema: Record) => void; +type ResolveSchemaRefFunction = (ref: string, visited?: Set) => Record; +type ResolveSchemaFunction = (schema: unknown, visited?: Set) => Record; +type ParseContentSchemaFunction = ( + contentType: string, + content: Record +) => [Record | null, string | null]; +type ParseRequestBodyFunction = ( + operation: OpenAPIV3.OperationObject +) => [Record | null, string | null]; +type GetParameterLocationFunction = (propSchema: Record) => ParameterLocation; +type ExtractOperationsFunction = ( + pathItem: OpenAPIV3.PathItemObject +) => [string, OpenAPIV3.OperationObject][]; +type ResolveParameterFunction = ( + param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject +) => OpenAPIV3.ParameterObject | null; + +// Type for accessing private properties/methods via type assertion +type PrivateAccess = { + baseUrl: string; + _isFileType: IsFileTypeFunction; + _convertToFileType: ConvertToFileTypeFunction; + _handleFileProperties: HandleFilePropertiesFunction; + _resolveSchemaRef: ResolveSchemaRefFunction; + _resolveSchema: ResolveSchemaFunction; + _parseContentSchema: ParseContentSchemaFunction; + _parseRequestBody: ParseRequestBodyFunction; + _getParameterLocation: GetParameterLocationFunction; + extractOperations: ExtractOperationsFunction; + resolveParameter: ResolveParameterFunction; +}; + describe('OpenAPIParser', () => { // Test initialization describe('constructor', () => { @@ -68,7 +107,7 @@ describe('OpenAPIParser', () => { const parser = new OpenAPIParser(mockCoreSpec, customBaseUrl); // We need to test this indirectly by checking the baseUrl property - expect((parser as any).baseUrl).toBe(customBaseUrl); + expect((parser as unknown as { baseUrl: string }).baseUrl).toBe(customBaseUrl); }); it('should correctly apply default base URL to parsed tools', () => { @@ -89,7 +128,7 @@ describe('OpenAPIParser', () => { execute: { headers: {}, method: 'GET', - url: `${(this as any).baseUrl}/core/users/{id}`, + url: `${(this as unknown as { baseUrl: string }).baseUrl}/core/users/{id}`, name: 'core_get_user', parameterLocations: { id: ParameterLocation.PATH }, }, @@ -127,7 +166,7 @@ describe('OpenAPIParser', () => { execute: { headers: {}, method: 'GET', - url: `${(this as any).baseUrl}/core/users/{id}`, + url: `${(this as unknown as { baseUrl: string }).baseUrl}/core/users/{id}`, name: 'core_get_user', parameterLocations: { id: ParameterLocation.PATH }, }, @@ -167,7 +206,7 @@ describe('OpenAPIParser', () => { execute: { headers: {}, method: 'GET', - url: `${(this as any).baseUrl}/test`, + url: `${(this as unknown as { baseUrl: string }).baseUrl}/test`, name: 'get_test', parameterLocations: {}, }, @@ -182,6 +221,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'get_test', summary: 'Test endpoint', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -218,7 +262,7 @@ describe('OpenAPIParser', () => { execute: { headers: {}, method: 'GET', - url: `${(this as any).baseUrl}/test`, + url: `${(this as unknown as { baseUrl: string }).baseUrl}/test`, name: 'get_test', parameterLocations: {}, }, @@ -233,6 +277,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'get_test', summary: 'Test endpoint', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -255,7 +304,7 @@ describe('OpenAPIParser', () => { // Test parseTools method with mock specs describe('parseTools', () => { // Create mock specs for each vertical - const mockSpecs = { + const mockSpecs: Record = { core: mockCoreSpec, crm: createMinimalSpec({ paths: { @@ -263,6 +312,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'crm_get_contact', summary: 'Get contact details', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -273,6 +327,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'documents_get', summary: 'Get document', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -283,6 +342,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'iam_get_user', summary: 'Get IAM user', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -293,6 +357,11 @@ describe('OpenAPIParser', () => { get: { operationId: 'lms_get_course', summary: 'Get course details', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, @@ -303,13 +372,19 @@ describe('OpenAPIParser', () => { get: { operationId: 'marketing_get_campaign', summary: 'Get campaign details', + responses: { + '200': { + description: 'OK', + }, + }, }, }, }, }), }; - Object.entries(mockSpecs).forEach(([specName, spec]) => { + // Use for...of instead of forEach + for (const [specName, spec] of Object.entries(mockSpecs)) { it(`should parse tools from ${specName} spec`, () => { const parser = new OpenAPIParser(spec); const tools = parser.parseTools(); @@ -339,7 +414,7 @@ describe('OpenAPIParser', () => { expect(tool.execute.parameterLocations).toBeDefined(); } }); - }); + } it('should throw error if operation ID is missing', () => { // Create a spec with missing operationId @@ -385,7 +460,7 @@ describe('OpenAPIParser', () => { const parser = new OpenAPIParser(createMinimalSpec()); // We need to access the private method, which requires a workaround - const isFileType = (parser as any)._isFileType.bind(parser); + const isFileType = (parser as unknown as PrivateAccess)._isFileType.bind(parser); expect(isFileType({ type: 'string', format: 'binary' })).toBe(true); expect(isFileType({ type: 'string' })).toBe(false); @@ -396,7 +471,9 @@ describe('OpenAPIParser', () => { describe('_convertToFileType', () => { it('should convert binary string schema to file type', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const convertToFileType = (parser as any)._convertToFileType.bind(parser); + const convertToFileType = (parser as unknown as PrivateAccess)._convertToFileType.bind( + parser + ); const schema = { type: 'string', format: 'binary' }; convertToFileType(schema); @@ -411,7 +488,9 @@ describe('OpenAPIParser', () => { describe('_handleFileProperties', () => { it('should process file properties in schema', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const handleFileProperties = (parser as any)._handleFileProperties.bind(parser); + const handleFileProperties = (parser as unknown as PrivateAccess)._handleFileProperties.bind( + parser + ); const schema = { properties: { @@ -449,7 +528,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithRefs); - const resolveSchemaRef = (parser as any)._resolveSchemaRef.bind(parser); + const resolveSchemaRef = (parser as unknown as PrivateAccess)._resolveSchemaRef.bind(parser); const resolved = resolveSchemaRef('#/components/schemas/TestSchema'); expect(resolved).toBeDefined(); @@ -479,7 +558,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithCircularRefs); - const resolveSchemaRef = (parser as any)._resolveSchemaRef.bind(parser); + const resolveSchemaRef = (parser as unknown as PrivateAccess)._resolveSchemaRef.bind(parser); expect(() => resolveSchemaRef('#/components/schemas/A')).toThrow(/Circular reference/); }); @@ -510,7 +589,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithRefs); - const resolveSchema = (parser as any)._resolveSchema.bind(parser); + const resolveSchema = (parser as unknown as PrivateAccess)._resolveSchema.bind(parser); const schema = { $ref: '#/components/schemas/Person' }; const resolved = resolveSchema(schema); @@ -551,7 +630,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithAllOf); - const resolveSchema = (parser as any)._resolveSchema.bind(parser); + const resolveSchema = (parser as unknown as PrivateAccess)._resolveSchema.bind(parser); const schema = { $ref: '#/components/schemas/Person' }; const resolved = resolveSchema(schema); @@ -566,7 +645,9 @@ describe('OpenAPIParser', () => { describe('_parseContentSchema', () => { it('should parse content schema for a specific content type', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const parseContentSchema = (parser as any)._parseContentSchema.bind(parser); + const parseContentSchema = (parser as unknown as PrivateAccess)._parseContentSchema.bind( + parser + ); const content = { 'application/json': { @@ -589,7 +670,9 @@ describe('OpenAPIParser', () => { it('should return null for missing content type', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const parseContentSchema = (parser as any)._parseContentSchema.bind(parser); + const parseContentSchema = (parser as unknown as PrivateAccess)._parseContentSchema.bind( + parser + ); const content = { 'application/json': { @@ -612,7 +695,7 @@ describe('OpenAPIParser', () => { describe('_parseRequestBody', () => { it('should parse JSON request body', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const parseRequestBody = (parser as any)._parseRequestBody.bind(parser); + const parseRequestBody = (parser as unknown as PrivateAccess)._parseRequestBody.bind(parser); const operation = { requestBody: { @@ -640,7 +723,7 @@ describe('OpenAPIParser', () => { it('should parse multipart form-data request body', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const parseRequestBody = (parser as any)._parseRequestBody.bind(parser); + const parseRequestBody = (parser as unknown as PrivateAccess)._parseRequestBody.bind(parser); const operation = { requestBody: { @@ -669,7 +752,7 @@ describe('OpenAPIParser', () => { it('should parse form-urlencoded request body', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const parseRequestBody = (parser as any)._parseRequestBody.bind(parser); + const parseRequestBody = (parser as unknown as PrivateAccess)._parseRequestBody.bind(parser); const operation = { requestBody: { @@ -717,7 +800,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithBodyRefs); - const parseRequestBody = (parser as any)._parseRequestBody.bind(parser); + const parseRequestBody = (parser as unknown as PrivateAccess)._parseRequestBody.bind(parser); const operation = { requestBody: { @@ -737,7 +820,9 @@ describe('OpenAPIParser', () => { describe('_getParameterLocation', () => { it('should determine parameter location based on schema type', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const getParameterLocation = (parser as any)._getParameterLocation.bind(parser); + const getParameterLocation = (parser as unknown as PrivateAccess)._getParameterLocation.bind( + parser + ); expect(getParameterLocation({ type: 'file' })).toBe(ParameterLocation.FILE); expect( @@ -755,16 +840,26 @@ describe('OpenAPIParser', () => { describe('extractOperations', () => { it('should extract operations from a path item', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const extractOperations = (parser as any).extractOperations.bind(parser); + const extractOperations = (parser as unknown as PrivateAccess).extractOperations.bind(parser); - const pathItem = { + const pathItem: OpenAPIV3.PathItemObject = { get: { operationId: 'get_test', summary: 'Get test', + responses: { + '200': { + description: 'OK', + }, + }, }, post: { operationId: 'create_test', summary: 'Create test', + responses: { + '200': { + description: 'OK', + }, + }, }, }; @@ -794,7 +889,7 @@ describe('OpenAPIParser', () => { }); const parser = new OpenAPIParser(specWithParamRefs); - const resolveParameter = (parser as any).resolveParameter.bind(parser); + const resolveParameter = (parser as unknown as PrivateAccess).resolveParameter.bind(parser); const paramRef = { $ref: '#/components/parameters/ApiVersion' }; const resolved = resolveParameter(paramRef); @@ -807,7 +902,7 @@ describe('OpenAPIParser', () => { it('should return the parameter if it is not a reference', () => { const parser = new OpenAPIParser(createMinimalSpec()); - const resolveParameter = (parser as any).resolveParameter.bind(parser); + const resolveParameter = (parser as unknown as PrivateAccess).resolveParameter.bind(parser); const param = { name: 'id',