diff --git a/components/atlas/README.md b/components/atlas/README.md new file mode 100644 index 0000000000000..30d74d258442c --- /dev/null +++ b/components/atlas/README.md @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/components/atlas/actions/get-all-jobs/get-all-jobs.mjs b/components/atlas/actions/get-all-jobs/get-all-jobs.mjs new file mode 100644 index 0000000000000..e3e321d46cc1e --- /dev/null +++ b/components/atlas/actions/get-all-jobs/get-all-jobs.mjs @@ -0,0 +1,52 @@ +import { atlasProps, jobProps } from "../../common/atlas-props.mjs"; +import { atlasMixin } from "../../common/atlas-base.mjs"; + +export default { + key: "atlas-get-all-jobs", + name: "Get All Job Listings", + description: "Retrieve all job listings from ATLAS", + version: "0.0.15", + type: "action", + props: { + ...atlasProps, + ...jobProps, + }, + ...atlasMixin, + async run({ $ }) { + try { + // Validate authentication configuration + this.validateAuth(); + + // Create ATLAS client + const atlas = this.createAtlasClient(); + + // Prepare parameters + const params = this.cleanParams({ + limit: this.limit, + offset: this.offset, + status: this.status, + }); + + // Make API request + const response = await atlas.getJobs(params); + + // Process response + const jobs = response.data || response; + const jobCount = Array.isArray(jobs) ? jobs.length : 0; + + const authMethod = this.apiKey ? "API Key" : "Username/Password"; + $.export("$summary", `Successfully retrieved ${jobCount} job listing(s) using ${authMethod}`); + + return { + success: true, + count: jobCount, + jobs: jobs, + params: params, + authMethod: authMethod, + }; + + } catch (error) { + this.handleAtlasError(error, "Get jobs"); + } + }, +}; \ No newline at end of file diff --git a/components/atlas/actions/get-candidates/get-candidates.mjs b/components/atlas/actions/get-candidates/get-candidates.mjs new file mode 100644 index 0000000000000..bf88c7ab2edd0 --- /dev/null +++ b/components/atlas/actions/get-candidates/get-candidates.mjs @@ -0,0 +1,47 @@ +import { atlasProps, candidateProps } from "../../common/atlas-props.mjs"; +import { atlasMixin } from "../../common/atlas-base.mjs"; + +export default { + key: "atlas-get-candidates", + name: "Get All Candidates", + description: "Retrieve all candidates from ATLAS", + version: "0.0.1", + type: "action", + props: { + ...atlasProps, + ...candidateProps, + }, + ...atlasMixin, + async run({ $ }) { + try { + // Validate authentication configuration + this.validateAuth(); + + const atlas = this.createAtlasClient(); + + const params = this.cleanParams({ + limit: this.limit, + stage: this.stage, + }); + + const response = await atlas.getCandidates(params); + + const candidates = response.data || response; + const candidateCount = Array.isArray(candidates) ? candidates.length : 0; + + const authMethod = this.apiKey ? "API Key" : "Username/Password"; + $.export("$summary", `Successfully retrieved ${candidateCount} candidate(s) using ${authMethod}`); + + return { + success: true, + count: candidateCount, + candidates: candidates, + params: params, + authMethod: authMethod, + }; + + } catch (error) { + this.handleAtlasError(error, "Get candidates"); + } + }, +}; \ No newline at end of file diff --git a/components/atlas/atlas.app.mjs b/components/atlas/atlas.app.mjs new file mode 100644 index 0000000000000..7284ae30d6530 --- /dev/null +++ b/components/atlas/atlas.app.mjs @@ -0,0 +1,109 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "atlas", + propDefinitions: { + apiKey: { + type: "string", + label: "API Key", + description: "Your ATLAS API key from the dashboard", + secret: true, + }, + baseUrl: { + type: "string", + label: "Base URL", + description: "ATLAS API base URL default: https://public-apis-prod.workland.com/api", + default: "https://public-apis-prod.workland.com/api", + optional: true, + }, + }, + methods: { + /** + * Get the authentication headers for API requests + * @returns {Object} Headers object with authorization + */ + _getHeaders() { + return { + "Authorization": `${this.$auth.api_key}`, + "Content-Type": "application/json", + "Accept": "application/json", + }; + }, + + /** + * Make authenticated API request + * @param {Object} opts - Request options + * @param {string} opts.url - API endpoint URL + * @param {string} [opts.method=GET] - HTTP method + * @param {Object} [opts.data] - Request body data + * @param {Object} [opts.params] - Query parameters + * @returns {Promise} API response + */ + async _makeRequest({ + url, + method = "GET", + data, + params, + ...opts + }) { + const config = { + method, + url: url.startsWith("http") ? url : `${this.$auth.base_url}${url}`, + headers: this._getHeaders(), + data, + params, + ...opts, + }; + + return axios(this, config); + }, + + /** + * Get all job listings from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Job listings response + */ + async getJobs(params = {}) { + return this._makeRequest({ + url: "/v3/jobs", + params, + }); + }, + + /** + * Get all candidates from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Candidates response + */ + async getCandidates(params = {}) { + return this._makeRequest({ + url: "/v3/candidates", + params, + }); + }, + + /** + * Get reports from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Reports response + */ + async getReports(params = {}) { + return this._makeRequest({ + url: "/v3/reports", + params, + }); + }, + + /** + * Test API connection + * @returns {Promise} Connection test response + */ + async testConnection() { + return this._makeRequest({ + url: "/v3/jobs", + params: { limit: 1 }, + }); + }, + }, +}; \ No newline at end of file diff --git a/components/atlas/common/atlas-base.mjs b/components/atlas/common/atlas-base.mjs new file mode 100644 index 0000000000000..18a5ad6054e50 --- /dev/null +++ b/components/atlas/common/atlas-base.mjs @@ -0,0 +1,310 @@ +import { axios } from "@pipedream/platform"; + +// Base class for all ATLAS interactions +export class AtlasBase { + constructor(apiKey, username, password, baseUrl, authUrl) { + this.apiKey = apiKey; + this.username = username; + this.password = password; + this.baseUrl = baseUrl; + this.authUrl = authUrl; + this.authToken = null; // Will store login token if using username/password + } + + /** + * Login using username/password to get auth token + * @returns {Promise} Authentication token + */ + async login() { + if (!this.username || !this.password) { + throw new Error("Username and password are required for login authentication"); + } + + try { + const response = await axios(this, { + method: "POST", + url: `${this.authUrl}/auth/login`, + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + data: { + email: this.username, + password: this.password, + }, + }); + + // Extract token from response (adjust based on actual API response structure) + this.authToken = response.data?.token || response.data?.access_token || response.token; + + if (!this.authToken) { + throw new Error("Login successful but no token received in response"); + } + + return this.authToken; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Login failed: Invalid username or password"); + } + throw new Error(`Login failed: ${error.message}`); + } + } + + /** + * Get authentication headers for API requests + * @returns {Promise} Headers object + */ + async getHeaders() { + let token; + let headers = { + "Content-Type": "application/json", + "Accept": "application/json", + }; + + if (this.apiKey) { + // Use API Key if provided + token = this.apiKey; + headers["Authorization"] = `${token}`; + } else { + // Use username/password login if no API key + if (!this.authToken) { + await this.login(); + } + token = this.authToken; + headers["Authorization"] = `Bearer ${token}`; + } + + return headers; + } + + /** + * Make authenticated API request + * @param {Object} options - Request options + * @param {string} options.url - API endpoint URL + * @param {string} [options.method=GET] - HTTP method + * @param {Object} [options.data] - Request body data + * @param {Object} [options.params] - Query parameters + * @param {Object} [options.headers] - Additional headers + * @returns {Promise} API response + */ + async makeRequest({ url, method = "GET", data, params, headers = {}, ...opts }) { + const authHeaders = await this.getHeaders(); + + const config = { + method, + url: url.startsWith("http") ? url : `${this.baseUrl}${url}`, + headers: { + ...authHeaders, + ...headers, + }, + data, + params, + ...opts, + }; + + console.log('confgig', config); + + try { + return await axios(this, config); + } catch (error) { + // If we get 401 and using login auth, try to re-login once + if (error.response?.status === 401 && !this.apiKey && this.authToken) { + console.log("Token expired, attempting re-login..."); + this.authToken = null; + const authHeaders = await this.getHeaders(); + + const retryConfig = { + ...config, + headers: { + ...authHeaders, + ...headers, + }, + }; + + return await axios(this, retryConfig); + } + + throw error; + } + } + + /** + * Get all job listings from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Job listings response + */ + async getJobs(params = {}) { + return this.makeRequest({ + url: "/v3/jobs", + params, + }); + } + + /** + * Get job details by ID + * @param {string|number} jobId - Job ID + * @returns {Promise} Job details response + */ + async getJob(jobId) { + return this.makeRequest({ + url: `/v3/jobs/${jobId}`, + }); + } + + /** + * Get all candidates from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Candidates response + */ + async getCandidates(params = {}) { + return this.makeRequest({ + url: "/v3/candidates", + params, + }); + } + + /** + * Get candidate details by ID + * @param {string|number} candidateId - Candidate ID + * @returns {Promise} Candidate details response + */ + async getCandidate(candidateId) { + return this.makeRequest({ + url: `/v3/candidates/${candidateId}`, + }); + } + + /** + * Get reports from ATLAS + * @param {Object} params - Query parameters + * @returns {Promise} Reports response + */ + async getReports(params = {}) { + return this.makeRequest({ + url: "/v3/reports", + params, + }); + } + + /** + * Test API connection + * @returns {Promise} Connection test response + */ + async testConnection() { + return this.makeRequest({ + url: "/v3/jobs", + params: { limit: 1 }, + }); + } + + /** + * Handle pagination for large datasets + * @param {string} endpoint - API endpoint + * @param {Object} params - Query parameters + * @param {number} maxPages - Maximum pages to fetch + * @returns {Promise} All results from paginated requests + */ + async getAllPaginated(endpoint, params = {}, maxPages = 10) { + const allResults = []; + let currentPage = 0; + let hasMore = true; + + while (hasMore && currentPage < maxPages) { + const response = await this.makeRequest({ + url: endpoint, + params: { + ...params, + offset: currentPage * (params.limit || 100), + limit: params.limit || 100, + }, + }); + + const results = response.data || response; + allResults.push(...results); + + // Check if there are more results + hasMore = results.length === (params.limit || 100); + currentPage++; + } + + return allResults; + } +} + +// Helper function to create ATLAS instance +export function createAtlasClient(apiKey, username, password, baseUrl, authUrl) { + return new AtlasBase(apiKey, username, password, baseUrl, authUrl); +} + +// Mixin for Pipedream components +export const atlasMixin = { + methods: { + /** + * Create ATLAS client instance + * @returns {AtlasBase} ATLAS client + */ + createAtlasClient() { + return new AtlasBase(this.apiKey, this.username, this.password, this.baseUrl, this.authUrl); + }, + + /** + * Validate authentication configuration + * @throws {Error} If neither API key nor username/password provided + */ + validateAuth() { + if (!this.apiKey && (!this.username || !this.password)) { + throw new Error( + "Authentication required: Please provide either an API Key OR both Username and Password" + ); + } + }, + + /** + * Handle API errors consistently + * @param {Error} error - The error object + * @param {string} operation - Description of the operation that failed + */ + handleAtlasError(error, operation = "API request") { + console.error(`ATLAS ${operation} failed:`, error); + + if (error.response) { + const status = error.response.status; + const message = error.response.data?.message || error.response.data?.error || error.message; + + switch (status) { + case 401: + throw new Error(`Authentication failed: Please check your credentials. ${message}`); + case 403: + throw new Error(`Access forbidden: Insufficient permissions for this operation. ${message}`); + case 404: + throw new Error(`Resource not found: ${message}`); + case 429: + throw new Error(`Rate limit exceeded: Please wait before making more requests. ${message}`); + case 500: + throw new Error(`ATLAS server error: ${message}`); + default: + throw new Error(`${operation} failed (${status}): ${message}`); + } + } + + throw new Error(`${operation} failed: ${error.message}`); + }, + + /** + * Validate and format API parameters + * @param {Object} params - Parameters to validate + * @returns {Object} Cleaned parameters + */ + cleanParams(params) { + const cleaned = {}; + + Object.entries(params).forEach(([key, value]) => { + if (value !== null && value !== undefined && value !== "") { + cleaned[key] = value; + } + }); + + return cleaned; + }, + }, +}; \ No newline at end of file diff --git a/components/atlas/common/atlas-props.mjs b/components/atlas/common/atlas-props.mjs new file mode 100644 index 0000000000000..155a419a8b2ce --- /dev/null +++ b/components/atlas/common/atlas-props.mjs @@ -0,0 +1,92 @@ +// Reusable prop definitions +export const atlasProps = { + apiKey: { + type: "string", + label: "ATLAS API Key", + description: "Your ATLAS API key from atlas.workland.com dashboard (leave empty to use username/password)", + secret: true, + optional: true, + }, + username: { + type: "string", + label: "Username", + description: "Your ATLAS username (required if no API key provided)", + optional: true, + }, + password: { + type: "string", + label: "Password", + description: "Your ATLAS password (required if no API key provided)", + secret: true, + optional: true, + }, + baseUrl: { + type: "string", + label: "Base URL", + description: "ATLAS API base URL", + default: "https://public-apis-prod.workland.com/api", + optional: true, + }, + authUrl: { + type: "string", + label: "Auth URL", + description: "ATLAS Auth base URL", + default: "https://user-accounts-prod.workland.com/api/v1", + optional: true, + }, +}; + +// Job-specific props +export const jobProps = { + limit: { + type: "integer", + label: "Limit", + description: "Maximum number of jobs to retrieve", + default: 500, + optional: true, + }, + offset: { + type: "integer", + label: "Offset", + description: "Number of jobs to skip (for pagination)", + default: 0, + optional: true, + }, + status: { + type: "string", + label: "Job Status", + description: "Filter jobs by status", + optional: true, + options: [ + "active", + "inactive", + "draft", + "closed", + ], + }, +}; + +// Candidate-specific props +export const candidateProps = { + limit: { + type: "integer", + label: "Limit", + description: "Maximum number of candidates to retrieve", + default: 100, + optional: true, + }, + stage: { + type: "string", + label: "Candidate Stage", + description: "Filter candidates by stage", + optional: true, + options: [ + "applied", + "screening", + "interview", + "offer", + "hired", + "rejected", + ], + }, +}; \ No newline at end of file diff --git a/components/atlas/package.json b/components/atlas/package.json new file mode 100644 index 0000000000000..eb7f48127bf7f --- /dev/null +++ b/components/atlas/package.json @@ -0,0 +1,27 @@ +{ + "name": "@pipedream/atlas", + "version": "0.0.1", + "description": "Pipedream ATLAS integration", + "main": "atlas.app.mjs", + "keywords": [ + "pipedream", + "atlas", + "workland", + "jobs", + "recruitment", + "hr" + ], + "homepage": "https://import.af/", + "author": "L'importateur (https://import.af/)", + "repository": { + "type": "git", + "url": "https://github.com/Import-AF/pipedream" + }, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@pipedream/platform": "^1.6.5" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/components/atlas/sources/new-job-posted/new-job-posted.mjs b/components/atlas/sources/new-job-posted/new-job-posted.mjs new file mode 100644 index 0000000000000..0df06425943ce --- /dev/null +++ b/components/atlas/sources/new-job-posted/new-job-posted.mjs @@ -0,0 +1,62 @@ +import { atlasProps } from "../../common/atlas-props.mjs"; +import { atlasMixin } from "../../common/atlas-base.mjs"; + +export default { + key: "atlas-new-job-posted", + name: "New Job Posted", + description: "Emit new event when a job is posted on ATLAS", + version: "0.0.1", + type: "source", + props: { + ...atlasProps, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 15, // Check every 15 minutes + }, + }, + }, + ...atlasMixin, + dedupe: "unique", + async run() { + try { + // Validate authentication configuration + this.validateAuth(); + + const atlas = this.createAtlasClient(); + + // Get the last check timestamp + const lastCheck = this.db.get("lastCheck") || new Date(Date.now() - 24 * 60 * 60 * 1000); // Default: 24h ago + + // Get recent jobs + const response = await atlas.getJobs({ + limit: 100, + // Add date filter if API supports it + created_after: lastCheck, + }); + + const jobs = response.data || response; + + if (Array.isArray(jobs)) { + // Emit each new job as separate event + jobs.forEach(job => { + this.$emit(job, { + id: job.id, + summary: `New job posted: ${job.title || job.name || 'Untitled Job'}`, + ts: job.created_at ? new Date(job.created_at).getTime() : Date.now(), + }); + }); + + // Update last check timestamp + this.db.set("lastCheck", new Date().toISOString()); + + const authMethod = this.apiKey ? "API Key" : "Username/Password"; + console.log(`Processed ${jobs.length} jobs using ${authMethod}`); + } + + } catch (error) { + this.handleAtlasError(error, "Check for new jobs"); + } + }, +}; \ No newline at end of file diff --git a/components/quickbooks/actions/create-invoice/create-invoice.mjs b/components/quickbooks/actions/create-invoice/create-invoice.mjs index 63791beca9f50..5bd3c0a12053a 100644 --- a/components/quickbooks/actions/create-invoice/create-invoice.mjs +++ b/components/quickbooks/actions/create-invoice/create-invoice.mjs @@ -6,7 +6,7 @@ export default { key: "quickbooks-create-invoice", name: "Create Invoice", description: "Creates an invoice. [See the documentation](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice)", - version: "0.2.2", + version: "0.2.3", type: "action", props: { quickbooks, @@ -28,6 +28,12 @@ export default { description: "Date when the payment of the transaction is due (YYYY-MM-DD)", optional: true, }, + termId: { + type: "integer", + label: "Term ID", + description: "Reference to the Term associated with the transaction. If specified, this takes precedence over DueDate for calculating payment due date.", + optional: true, + }, allowOnlineCreditCardPayment: { type: "boolean", label: "Allow Online Credit Card Payment", @@ -161,6 +167,11 @@ export default { throw new ConfigurationError("Must provide lineItems, and customerRefValue parameters."); } + // Validate that termId and dueDate are not both provided + if (this.termId && this.dueDate) { + $.export("$summary", "Warning: Both termId and dueDate provided. termId takes precedence over dueDate."); + } + const lines = this.lineItemsAsObjects ? parseLineItems(this.lineItems) : this.buildLineItems(); @@ -177,7 +188,6 @@ export default { CustomerRef: { value: this.customerRefValue, }, - DueDate: this.dueDate, AllowOnlineCreditCardPayment: this.allowOnlineCreditCardPayment, AllowOnlineACHPayment: this.allowOnlineACHPayment, DocNumber: this.docNumber, @@ -187,6 +197,16 @@ export default { PrivateNote: this.privateNote, }; + // Add Term reference if termId is provided + if (this.termId) { + data.SalesTermRef = { + value: this.termId.toString(), + }; + } else if (this.dueDate) { + // Only set DueDate if termId is not provided + data.DueDate = this.dueDate; + } + if (this.billEmail) { params.include = "invoiceLink"; data.BillEmail = { @@ -216,4 +236,4 @@ export default { return response; }, -}; +}; \ No newline at end of file diff --git a/tl-s-pp-mjs.js b/tl-s-pp-mjs.js new file mode 100644 index 0000000000000..6e2b950f049c5 --- /dev/null +++ b/tl-s-pp-mjs.js @@ -0,0 +1,3 @@ +module.exports = { + name: "tl-s-pp-mjs", +}