@nowgetitdone/sdk
Advanced tools
+38
-0
| # @nowgetitdone/sdk | ||
| ## 0.2.0 | ||
| ### Minor Changes | ||
| - fb714a5: Task dependencies (blockers). Three new task methods reach the blocker graph: | ||
| `tasks.listDependencies(taskId)` answers both directions one hop out | ||
| (`blocked_by`, `blocking`) plus the derived `is_blocked`, | ||
| `tasks.addDependency(taskId, { blocker_task_id })` records a link, and | ||
| `tasks.removeDependency(taskId, blockerTaskId)` removes one — each answering | ||
| the task's full dependency view, and each safe to retry. A link that would | ||
| close a loop throws `ConflictError` with the new `dependency_cycle` code. That | ||
| 409 is now excluded from the retry policy: only `idempotency_in_progress` is | ||
| retried, so a permanent conflict no longer costs three calls of your burst | ||
| allowance. New exported types: `TaskDependencies`, `TaskDependencyRef`, | ||
| `AddTaskDependencyBody`. | ||
| - 42489cd: Tasks now carry a `start_date` (defer-until). `Task` and `TaskHistoryEntry` | ||
| gain a nullable `start_date`, and `CreateTaskBody` / `UpdateTaskBody` accept an | ||
| optional `start_date` as `YYYY-MM-DD` (interpreted as UTC). Like `due_date`, it | ||
| cannot be cleared through an update — an omitted field keeps its current value. | ||
| - bb12183: Recurring tasks. `Task` gains a nullable `recurrence` (the repeat rule plus its | ||
| derived `rrule` projection), and two new task methods reach the per-day | ||
| occurrence ledger: `tasks.listOccurrences(taskId, query)` pages the days a task | ||
| is due (filterable by `status`, `starting_on` and `ending_on`), and | ||
| `tasks.setOccurrenceStatus(taskId, 'YYYY-MM-DD', { status })` — with | ||
| `completeOccurrence` / `skipOccurrence` shorthands — closes ONE day. The day may | ||
| be in the past or a scheduled future day finished early; neither touches any | ||
| other day or the task's own status. New exported types: `TaskRecurrence`, | ||
| `TaskOccurrence`, `ListTaskOccurrencesQuery`, `SetTaskOccurrenceStatusBody`. | ||
| ### Patch Changes | ||
| - b8fa274: Deprecate `authStyle`: the /v1 API is Bearer-only, so `authStyle: 'x-api-key'` | ||
| could never authenticate — it answered `401 missing_credentials` before the key | ||
| was read (DevinoSolutions/getitdone-sdk#1). Every request now sends | ||
| `Authorization: Bearer …`; passing `'x-api-key'` logs a deprecation warning and | ||
| is ignored, turning a guaranteed failure into a working call. The option is | ||
| removed in 0.2.0. | ||
| ## 0.1.0 | ||
@@ -4,0 +42,0 @@ |
+95
-10
@@ -193,3 +193,3 @@ "use strict"; | ||
| // package.json | ||
| var version = "0.1.0"; | ||
| var version = "0.2.0"; | ||
@@ -216,4 +216,6 @@ // src/version.ts | ||
| var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]); | ||
| function isRetryableStatus(status, idempotencyKeySent) { | ||
| if (status === 409) return idempotencyKeySent; | ||
| function isRetryableStatus(status, idempotencyKeySent, problemCode) { | ||
| if (status === 409) { | ||
| return idempotencyKeySent && problemCode === "idempotency_in_progress"; | ||
| } | ||
| return RETRYABLE_STATUSES.has(status) || status >= 500; | ||
@@ -298,3 +300,4 @@ } | ||
| response.status, | ||
| idempotencyKey !== void 0 | ||
| idempotencyKey !== void 0, | ||
| problem?.code | ||
| ) && (retryAfterSeconds === null || retryAfterSeconds <= this.config.maxRetryAfterSeconds)) { | ||
@@ -352,7 +355,3 @@ const delayMs = retryAfterSeconds !== null ? retryAfterSeconds * 1e3 : backoffMs(attempt); | ||
| }; | ||
| if (this.config.authStyle === "x-api-key") { | ||
| headers["x-api-key"] = this.config.apiKey; | ||
| } else { | ||
| headers["authorization"] = `Bearer ${this.config.apiKey}`; | ||
| } | ||
| headers["authorization"] = `Bearer ${this.config.apiKey}`; | ||
| if (params.body !== void 0) { | ||
@@ -730,2 +729,85 @@ headers["content-type"] = "application/json"; | ||
| } | ||
| /** | ||
| * The per-day ledger of a repeating task, most recent day first. A task | ||
| * that does not repeat pages empty rather than 404ing. | ||
| */ | ||
| listOccurrences(taskId, query, options) { | ||
| return requestPage(this._core, { | ||
| method: "GET", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/occurrences`, | ||
| query, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Complete or skip ONE day, addressed by its calendar day (`YYYY-MM-DD`). | ||
| * The day may be in the past or a scheduled future day finished early — | ||
| * neither touches any other day or the task's own status. Re-sending the | ||
| * state a day is already in succeeds unchanged, so retries are safe. | ||
| */ | ||
| setOccurrenceStatus(taskId, occurrenceDate, body, options) { | ||
| return this._core.request({ | ||
| method: "PATCH", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/occurrences/${encodeURIComponent(occurrenceDate)}`, | ||
| body, | ||
| options | ||
| }); | ||
| } | ||
| /** `setOccurrenceStatus(taskId, day, { status: 'COMPLETED' })`. */ | ||
| completeOccurrence(taskId, occurrenceDate, options) { | ||
| return this.setOccurrenceStatus( | ||
| taskId, | ||
| occurrenceDate, | ||
| { status: "COMPLETED" }, | ||
| options | ||
| ); | ||
| } | ||
| /** `setOccurrenceStatus(taskId, day, { status: 'SKIPPED' })`. */ | ||
| skipOccurrence(taskId, occurrenceDate, options) { | ||
| return this.setOccurrenceStatus( | ||
| taskId, | ||
| occurrenceDate, | ||
| { status: "SKIPPED" }, | ||
| options | ||
| ); | ||
| } | ||
| /** | ||
| * What this task is blocked by and what it blocks, plus the derived | ||
| * `is_blocked`. Not paginated — it answers the task's immediate | ||
| * neighbours; walk the chain by following each `task_id`. A task with no | ||
| * links answers an empty view rather than throwing `NotFoundError`. | ||
| */ | ||
| listDependencies(taskId, options) { | ||
| return this._core.request({ | ||
| method: "GET", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies`, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Record that this task is blocked by another one. Recording a link that | ||
| * already exists succeeds unchanged. A link that would close a loop throws | ||
| * `ConflictError` with code `dependency_cycle` — the graph you read is | ||
| * always acyclic. Answers the task's dependencies after the link. | ||
| */ | ||
| addDependency(taskId, body, options) { | ||
| return this._core.request({ | ||
| method: "POST", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies`, | ||
| body, | ||
| idempotent: true, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Remove one blocked-by link, addressed by both task ids — no edge id is | ||
| * ever minted. Removing a link that is not there succeeds unchanged. | ||
| */ | ||
| removeDependency(taskId, blockerTaskId, options) { | ||
| return this._core.request({ | ||
| method: "DELETE", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies/${encodeURIComponent(blockerTaskId)}`, | ||
| options | ||
| }); | ||
| } | ||
| }; | ||
@@ -860,2 +942,3 @@ | ||
| var DEFAULT_MAX_RETRY_AFTER_SECONDS = 60; | ||
| var X_API_KEY_AUTH_STYLE_DEPRECATION_MESSAGE = "[@nowgetitdone/sdk] `authStyle: 'x-api-key'` is deprecated and IGNORED. The /v1 API is Bearer-only: it answers 401 missing_credentials to an x-api-key header without ever reading the key, so every call under that style failed. Sending `Authorization: Bearer \u2026` instead. Remove the option \u2014 it is deleted in 0.2.0."; | ||
| function readEnv(name) { | ||
@@ -893,2 +976,5 @@ if (typeof process === "undefined") return void 0; | ||
| } | ||
| if (options.authStyle === "x-api-key") { | ||
| console.warn(X_API_KEY_AUTH_STYLE_DEPRECATION_MESSAGE); | ||
| } | ||
| this.baseUrl = (options.baseUrl ?? readEnv("GETITDONE_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, ""); | ||
@@ -901,3 +987,2 @@ this.core = new HttpCore({ | ||
| maxRetryAfterSeconds: options.maxRetryAfterSeconds ?? DEFAULT_MAX_RETRY_AFTER_SECONDS, | ||
| authStyle: options.authStyle ?? "authorization", | ||
| fetchFn: options.fetch ?? globalThis.fetch, | ||
@@ -904,0 +989,0 @@ defaultHeaders: options.defaultHeaders ?? {}, |
+95
-10
@@ -145,3 +145,3 @@ // src/error.ts | ||
| // package.json | ||
| var version = "0.1.0"; | ||
| var version = "0.2.0"; | ||
@@ -168,4 +168,6 @@ // src/version.ts | ||
| var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]); | ||
| function isRetryableStatus(status, idempotencyKeySent) { | ||
| if (status === 409) return idempotencyKeySent; | ||
| function isRetryableStatus(status, idempotencyKeySent, problemCode) { | ||
| if (status === 409) { | ||
| return idempotencyKeySent && problemCode === "idempotency_in_progress"; | ||
| } | ||
| return RETRYABLE_STATUSES.has(status) || status >= 500; | ||
@@ -250,3 +252,4 @@ } | ||
| response.status, | ||
| idempotencyKey !== void 0 | ||
| idempotencyKey !== void 0, | ||
| problem?.code | ||
| ) && (retryAfterSeconds === null || retryAfterSeconds <= this.config.maxRetryAfterSeconds)) { | ||
@@ -304,7 +307,3 @@ const delayMs = retryAfterSeconds !== null ? retryAfterSeconds * 1e3 : backoffMs(attempt); | ||
| }; | ||
| if (this.config.authStyle === "x-api-key") { | ||
| headers["x-api-key"] = this.config.apiKey; | ||
| } else { | ||
| headers["authorization"] = `Bearer ${this.config.apiKey}`; | ||
| } | ||
| headers["authorization"] = `Bearer ${this.config.apiKey}`; | ||
| if (params.body !== void 0) { | ||
@@ -682,2 +681,85 @@ headers["content-type"] = "application/json"; | ||
| } | ||
| /** | ||
| * The per-day ledger of a repeating task, most recent day first. A task | ||
| * that does not repeat pages empty rather than 404ing. | ||
| */ | ||
| listOccurrences(taskId, query, options) { | ||
| return requestPage(this._core, { | ||
| method: "GET", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/occurrences`, | ||
| query, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Complete or skip ONE day, addressed by its calendar day (`YYYY-MM-DD`). | ||
| * The day may be in the past or a scheduled future day finished early — | ||
| * neither touches any other day or the task's own status. Re-sending the | ||
| * state a day is already in succeeds unchanged, so retries are safe. | ||
| */ | ||
| setOccurrenceStatus(taskId, occurrenceDate, body, options) { | ||
| return this._core.request({ | ||
| method: "PATCH", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/occurrences/${encodeURIComponent(occurrenceDate)}`, | ||
| body, | ||
| options | ||
| }); | ||
| } | ||
| /** `setOccurrenceStatus(taskId, day, { status: 'COMPLETED' })`. */ | ||
| completeOccurrence(taskId, occurrenceDate, options) { | ||
| return this.setOccurrenceStatus( | ||
| taskId, | ||
| occurrenceDate, | ||
| { status: "COMPLETED" }, | ||
| options | ||
| ); | ||
| } | ||
| /** `setOccurrenceStatus(taskId, day, { status: 'SKIPPED' })`. */ | ||
| skipOccurrence(taskId, occurrenceDate, options) { | ||
| return this.setOccurrenceStatus( | ||
| taskId, | ||
| occurrenceDate, | ||
| { status: "SKIPPED" }, | ||
| options | ||
| ); | ||
| } | ||
| /** | ||
| * What this task is blocked by and what it blocks, plus the derived | ||
| * `is_blocked`. Not paginated — it answers the task's immediate | ||
| * neighbours; walk the chain by following each `task_id`. A task with no | ||
| * links answers an empty view rather than throwing `NotFoundError`. | ||
| */ | ||
| listDependencies(taskId, options) { | ||
| return this._core.request({ | ||
| method: "GET", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies`, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Record that this task is blocked by another one. Recording a link that | ||
| * already exists succeeds unchanged. A link that would close a loop throws | ||
| * `ConflictError` with code `dependency_cycle` — the graph you read is | ||
| * always acyclic. Answers the task's dependencies after the link. | ||
| */ | ||
| addDependency(taskId, body, options) { | ||
| return this._core.request({ | ||
| method: "POST", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies`, | ||
| body, | ||
| idempotent: true, | ||
| options | ||
| }); | ||
| } | ||
| /** | ||
| * Remove one blocked-by link, addressed by both task ids — no edge id is | ||
| * ever minted. Removing a link that is not there succeeds unchanged. | ||
| */ | ||
| removeDependency(taskId, blockerTaskId, options) { | ||
| return this._core.request({ | ||
| method: "DELETE", | ||
| path: `/v1/tasks/${encodeURIComponent(taskId)}/dependencies/${encodeURIComponent(blockerTaskId)}`, | ||
| options | ||
| }); | ||
| } | ||
| }; | ||
@@ -812,2 +894,3 @@ | ||
| var DEFAULT_MAX_RETRY_AFTER_SECONDS = 60; | ||
| var X_API_KEY_AUTH_STYLE_DEPRECATION_MESSAGE = "[@nowgetitdone/sdk] `authStyle: 'x-api-key'` is deprecated and IGNORED. The /v1 API is Bearer-only: it answers 401 missing_credentials to an x-api-key header without ever reading the key, so every call under that style failed. Sending `Authorization: Bearer \u2026` instead. Remove the option \u2014 it is deleted in 0.2.0."; | ||
| function readEnv(name) { | ||
@@ -845,2 +928,5 @@ if (typeof process === "undefined") return void 0; | ||
| } | ||
| if (options.authStyle === "x-api-key") { | ||
| console.warn(X_API_KEY_AUTH_STYLE_DEPRECATION_MESSAGE); | ||
| } | ||
| this.baseUrl = (options.baseUrl ?? readEnv("GETITDONE_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, ""); | ||
@@ -853,3 +939,2 @@ this.core = new HttpCore({ | ||
| maxRetryAfterSeconds: options.maxRetryAfterSeconds ?? DEFAULT_MAX_RETRY_AFTER_SECONDS, | ||
| authStyle: options.authStyle ?? "authorization", | ||
| fetchFn: options.fetch ?? globalThis.fetch, | ||
@@ -856,0 +941,0 @@ defaultHeaders: options.defaultHeaders ?? {}, |
+3
-4
| { | ||
| "name": "@nowgetitdone/sdk", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Official TypeScript SDK for the GetItDone public API (https://app.nowgetitdone.com/v1).", | ||
@@ -50,4 +50,3 @@ "license": "MIT", | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "provenance": false | ||
| "access": "public" | ||
| }, | ||
@@ -64,4 +63,4 @@ "dependencies": { | ||
| "@getitdone/api-contracts": "0.0.0", | ||
| "@getitdone/auth-scopes": "0.0.0", | ||
| "@getitdone/eslint-config": "0.0.0", | ||
| "@getitdone/auth-scopes": "0.0.0", | ||
| "@getitdone/typescript-config": "0.0.0" | ||
@@ -68,0 +67,0 @@ }, |
+10
-1
@@ -148,6 +148,15 @@ # @nowgetitdone/sdk | ||
| | `maxRetryAfterSeconds` | `60` | larger `Retry-After` ⇒ give up | | ||
| | `authStyle` | `'authorization'` | or `'x-api-key'` | | ||
| | `authStyle` | `'authorization'` | **deprecated** — see below; removed in 0.2.0 | | ||
| | `logger` | none | redacted request/response/retry events | | ||
| | `dangerouslyAllowBrowser` | `false` | API keys are secrets — keep them server-side | | ||
| ### `authStyle` is deprecated | ||
| The `/v1` API is **Bearer-only**: it reads `Authorization: Bearer gid_…` and | ||
| nothing else. `authStyle: 'x-api-key'` never worked — that header is a legacy | ||
| `/api/*` scheme, and `/v1` answers `401 missing_credentials` without ever | ||
| looking at the key. The SDK now logs a deprecation warning and sends Bearer | ||
| anyway, so calls that previously failed 100% of the time succeed. Drop the | ||
| option; it is removed in 0.2.0. | ||
| ## Development | ||
@@ -154,0 +163,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
525755
14.39%4833
13.61%171
5.56%