@ycadence/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/client.d.ts +123 -122
- package/dist/client.d.ts.map +1 -1
- package/dist/errors.d.ts +100 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +457 -64
- package/dist/index.js.map +9 -4
- package/dist/operations.d.ts +128 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/pagination.d.ts +13 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/client.ts +393 -135
- package/src/errors.ts +144 -0
- package/src/index.ts +31 -4
- package/src/operations.ts +74 -0
- package/src/pagination.ts +36 -0
- package/src/types.ts +368 -0
- package/src/version.ts +11 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors the client throws.
|
|
3
|
+
*
|
|
4
|
+
* One base class, `YCadenceError`, so `instanceof YCadenceError` catches every
|
|
5
|
+
* failure the client produces, and a subclass per kind of failure, so a caller
|
|
6
|
+
* can branch on the class instead of comparing status codes. The base keeps its
|
|
7
|
+
* original positional constructor `(status, code, message, issues)`, because
|
|
8
|
+
* integrations construct it in their own tests.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ApiIssue {
|
|
12
|
+
path: string
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The error body every v1 route returns (`packages/api/src/types.ts`). */
|
|
17
|
+
export interface ApiFailure {
|
|
18
|
+
error: string
|
|
19
|
+
code: string
|
|
20
|
+
issues?: ApiIssue[]
|
|
21
|
+
/** Added by the work management contracts; absent on older servers. */
|
|
22
|
+
details?: Record<string, unknown>
|
|
23
|
+
requestId?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ErrorExtras {
|
|
27
|
+
/** The server's request id, from the body or the `x-request-id` header. */
|
|
28
|
+
requestId?: string
|
|
29
|
+
details?: Record<string, unknown>
|
|
30
|
+
/** Seconds, from `Retry-After`, when the server sent one. */
|
|
31
|
+
retryAfter?: number
|
|
32
|
+
method?: string
|
|
33
|
+
path?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class YCadenceError extends Error {
|
|
37
|
+
readonly requestId: string | undefined
|
|
38
|
+
readonly details: Record<string, unknown> | undefined
|
|
39
|
+
readonly retryAfter: number | undefined
|
|
40
|
+
readonly method: string | undefined
|
|
41
|
+
readonly path: string | undefined
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
readonly status: number,
|
|
45
|
+
readonly code: string,
|
|
46
|
+
message: string,
|
|
47
|
+
readonly issues?: ApiIssue[],
|
|
48
|
+
extras: ErrorExtras = {}
|
|
49
|
+
) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.name = 'YCadenceError'
|
|
52
|
+
this.requestId = extras.requestId
|
|
53
|
+
this.details = extras.details
|
|
54
|
+
this.retryAfter = extras.retryAfter
|
|
55
|
+
this.method = extras.method
|
|
56
|
+
this.path = extras.path
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether retrying unchanged could plausibly succeed: rate limits, server
|
|
61
|
+
* errors, timeouts and an unreachable host. A 4xx means the request itself
|
|
62
|
+
* needs to change.
|
|
63
|
+
*/
|
|
64
|
+
get retryable(): boolean {
|
|
65
|
+
return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 400 or 422: the request was understood and refused as invalid. */
|
|
70
|
+
export class YCadenceValidationError extends YCadenceError {
|
|
71
|
+
override name = 'YCadenceValidationError'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 401 or 403: no credential, a bad one, or not allowed. */
|
|
75
|
+
export class YCadencePermissionError extends YCadenceError {
|
|
76
|
+
override name = 'YCadencePermissionError'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 404: missing, deleted, or in another organization. */
|
|
80
|
+
export class YCadenceNotFoundError extends YCadenceError {
|
|
81
|
+
override name = 'YCadenceNotFoundError'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 409: a domain rule, a version conflict, or the same idempotency key still running. */
|
|
85
|
+
export class YCadenceConflictError extends YCadenceError {
|
|
86
|
+
override name = 'YCadenceConflictError'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 412: an `If-Match` precondition failed; reload and try again. */
|
|
90
|
+
export class YCadencePreconditionError extends YCadenceError {
|
|
91
|
+
override name = 'YCadencePreconditionError'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 429: slow down; `retryAfter` says for how long when the server said. */
|
|
95
|
+
export class YCadenceRateLimitError extends YCadenceError {
|
|
96
|
+
override name = 'YCadenceRateLimitError'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Status 0 or 408: the host could not be reached, or did not answer in time. */
|
|
100
|
+
export class YCadenceNetworkError extends YCadenceError {
|
|
101
|
+
override name = 'YCadenceNetworkError'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The class that matches a status. */
|
|
105
|
+
export function errorClassFor(status: number): typeof YCadenceError {
|
|
106
|
+
if (status === 0 || status === 408) return YCadenceNetworkError
|
|
107
|
+
if (status === 400 || status === 422) return YCadenceValidationError
|
|
108
|
+
if (status === 401 || status === 403) return YCadencePermissionError
|
|
109
|
+
if (status === 404) return YCadenceNotFoundError
|
|
110
|
+
if (status === 409) return YCadenceConflictError
|
|
111
|
+
if (status === 412) return YCadencePreconditionError
|
|
112
|
+
if (status === 429) return YCadenceRateLimitError
|
|
113
|
+
return YCadenceError
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createError(
|
|
117
|
+
status: number,
|
|
118
|
+
code: string,
|
|
119
|
+
message: string,
|
|
120
|
+
issues?: ApiIssue[],
|
|
121
|
+
extras: ErrorExtras = {}
|
|
122
|
+
): YCadenceError {
|
|
123
|
+
const ErrorClass = errorClassFor(status)
|
|
124
|
+
return new ErrorClass(status, code, message, issues, extras)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Thrown, unchanged, through the client by a custom `fetch` that wants to stop
|
|
129
|
+
* a request without it being reported as a network error. The CLI's
|
|
130
|
+
* `--dry-run` uses it to show a mutation instead of sending it.
|
|
131
|
+
*/
|
|
132
|
+
export class RequestIntercepted extends Error {
|
|
133
|
+
override name = 'RequestIntercepted'
|
|
134
|
+
constructor(
|
|
135
|
+
readonly request: {
|
|
136
|
+
method: string
|
|
137
|
+
url: string
|
|
138
|
+
headers: Record<string, string>
|
|
139
|
+
body: unknown
|
|
140
|
+
}
|
|
141
|
+
) {
|
|
142
|
+
super(`${request.method} ${request.url} was not sent.`)
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
export {
|
|
2
|
-
type ApiFailure,
|
|
3
2
|
type ClientOptions,
|
|
4
3
|
DevPulseClient,
|
|
5
4
|
DevPulseError,
|
|
6
|
-
|
|
7
|
-
type
|
|
5
|
+
parseRetryAfter,
|
|
6
|
+
type RequestOptions,
|
|
7
|
+
type ResponseInfo,
|
|
8
|
+
type RetryOptions,
|
|
8
9
|
YCadenceClient,
|
|
9
|
-
YCadenceError,
|
|
10
10
|
} from './client.js'
|
|
11
|
+
export {
|
|
12
|
+
type ApiFailure,
|
|
13
|
+
type ApiIssue,
|
|
14
|
+
createError,
|
|
15
|
+
type ErrorExtras,
|
|
16
|
+
errorClassFor,
|
|
17
|
+
RequestIntercepted,
|
|
18
|
+
YCadenceConflictError,
|
|
19
|
+
YCadenceError,
|
|
20
|
+
YCadenceNetworkError,
|
|
21
|
+
YCadenceNotFoundError,
|
|
22
|
+
YCadencePermissionError,
|
|
23
|
+
YCadencePreconditionError,
|
|
24
|
+
YCadenceRateLimitError,
|
|
25
|
+
YCadenceValidationError,
|
|
26
|
+
} from './errors.js'
|
|
27
|
+
export {
|
|
28
|
+
buildPath,
|
|
29
|
+
type HttpMethod,
|
|
30
|
+
isIdempotentMethod,
|
|
31
|
+
OPERATIONS,
|
|
32
|
+
type Operation,
|
|
33
|
+
type OperationId,
|
|
34
|
+
} from './operations.js'
|
|
35
|
+
export { collect, paginate } from './pagination.js'
|
|
36
|
+
export * from './types.js'
|
|
37
|
+
export { SDK_VERSION } from './version.js'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every API operation the client calls, in one table.
|
|
3
|
+
*
|
|
4
|
+
* The client builds each request from this table rather than from a path
|
|
5
|
+
* written inline in a method, so there is one place to compare with the
|
|
6
|
+
* server: `apps/web/lib/api/sdk-contract.test.ts` fails when an entry has no
|
|
7
|
+
* matching route contract (unless it is marked `pending`), and the CLI's
|
|
8
|
+
* reference and `--dry-run` name operations by these ids.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
|
|
12
|
+
|
|
13
|
+
export interface Operation {
|
|
14
|
+
method: HttpMethod
|
|
15
|
+
/** The route contract's path, with `{param}` placeholders. */
|
|
16
|
+
path: string
|
|
17
|
+
/**
|
|
18
|
+
* Planned by the work management contracts and not served yet. The client
|
|
19
|
+
* still calls it; a 404 from an older server is reported as such.
|
|
20
|
+
*/
|
|
21
|
+
pending?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const OPERATIONS = {
|
|
25
|
+
health: { method: 'GET', path: '/api/health' },
|
|
26
|
+
me: { method: 'GET', path: '/api/v1/me' },
|
|
27
|
+
listProjects: { method: 'GET', path: '/api/v1/projects' },
|
|
28
|
+
|
|
29
|
+
listWorkItems: { method: 'GET', path: '/api/v1/projects/{projectId}/work-items' },
|
|
30
|
+
createWorkItem: { method: 'POST', path: '/api/v1/projects/{projectId}/work-items' },
|
|
31
|
+
getWorkItem: { method: 'GET', path: '/api/v1/work-items/{workItemId}' },
|
|
32
|
+
updateWorkItem: { method: 'PATCH', path: '/api/v1/work-items/{workItemId}' },
|
|
33
|
+
deleteWorkItem: { method: 'DELETE', path: '/api/v1/work-items/{workItemId}' },
|
|
34
|
+
|
|
35
|
+
listStates: { method: 'GET', path: '/api/v1/projects/{projectId}/states' },
|
|
36
|
+
listLabels: { method: 'GET', path: '/api/v1/projects/{projectId}/labels' },
|
|
37
|
+
createLabel: { method: 'POST', path: '/api/v1/projects/{projectId}/labels' },
|
|
38
|
+
listTypes: { method: 'GET', path: '/api/v1/projects/{projectId}/types' },
|
|
39
|
+
|
|
40
|
+
listCycles: { method: 'GET', path: '/api/v1/projects/{projectId}/cycles' },
|
|
41
|
+
createCycle: { method: 'POST', path: '/api/v1/projects/{projectId}/cycles' },
|
|
42
|
+
getCycleProgress: { method: 'GET', path: '/api/v1/cycles/{cycleId}/progress' },
|
|
43
|
+
addWorkItemsToCycle: { method: 'POST', path: '/api/v1/cycles/{cycleId}/work-items' },
|
|
44
|
+
|
|
45
|
+
listViews: { method: 'GET', path: '/api/v1/projects/{projectId}/views' },
|
|
46
|
+
createView: { method: 'POST', path: '/api/v1/projects/{projectId}/views' },
|
|
47
|
+
|
|
48
|
+
listIntake: { method: 'GET', path: '/api/v1/projects/{projectId}/intake' },
|
|
49
|
+
submitIntake: { method: 'POST', path: '/api/v1/projects/{projectId}/intake' },
|
|
50
|
+
triageIntake: { method: 'POST', path: '/api/v1/intake-items/{intakeItemId}/triage' },
|
|
51
|
+
|
|
52
|
+
previewImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports/preview' },
|
|
53
|
+
startImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports' },
|
|
54
|
+
listImports: { method: 'GET', path: '/api/v1/projects/{projectId}/imports' },
|
|
55
|
+
getImport: { method: 'GET', path: '/api/v1/imports/{runId}' },
|
|
56
|
+
} as const satisfies Record<string, Operation>
|
|
57
|
+
|
|
58
|
+
export type OperationId = keyof typeof OPERATIONS
|
|
59
|
+
|
|
60
|
+
/** GET and HEAD are safe to repeat; anything else needs an idempotency key first. */
|
|
61
|
+
export function isIdempotentMethod(method: string): boolean {
|
|
62
|
+
return method === 'GET' || method === 'HEAD'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Fill `{param}` placeholders, encoding each value. */
|
|
66
|
+
export function buildPath(template: string, params: Record<string, string> = {}): string {
|
|
67
|
+
return template.replace(/\{(\w+)\}/g, (_match, name: string) => {
|
|
68
|
+
const value = params[name]
|
|
69
|
+
if (value === undefined || value === '') {
|
|
70
|
+
throw new TypeError(`Missing path parameter "${name}" for ${template}.`)
|
|
71
|
+
}
|
|
72
|
+
return encodeURIComponent(value)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Page } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Follow `nextCursor` to the end, one page at a time.
|
|
5
|
+
*
|
|
6
|
+
* Lazy: a caller that breaks out early pays for the pages it read and no more.
|
|
7
|
+
*/
|
|
8
|
+
export async function* paginate<T>(
|
|
9
|
+
fetchPage: (cursor: string | undefined) => Promise<Page<T>>,
|
|
10
|
+
options: { cursor?: string; maxItems?: number } = {}
|
|
11
|
+
): AsyncGenerator<T> {
|
|
12
|
+
let cursor = options.cursor
|
|
13
|
+
let yielded = 0
|
|
14
|
+
do {
|
|
15
|
+
const page = await fetchPage(cursor)
|
|
16
|
+
for (const item of page.items) {
|
|
17
|
+
if (options.maxItems !== undefined && yielded >= options.maxItems) return
|
|
18
|
+
yielded += 1
|
|
19
|
+
yield item
|
|
20
|
+
}
|
|
21
|
+
cursor = page.nextCursor ?? undefined
|
|
22
|
+
} while (cursor !== undefined)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every item from {@link paginate}, as an array, up to `maxItems`. */
|
|
26
|
+
export async function collect<T>(
|
|
27
|
+
iterable: AsyncIterable<T>,
|
|
28
|
+
maxItems = Number.POSITIVE_INFINITY
|
|
29
|
+
): Promise<T[]> {
|
|
30
|
+
const items: T[] = []
|
|
31
|
+
for await (const item of iterable) {
|
|
32
|
+
if (items.length >= maxItems) break
|
|
33
|
+
items.push(item)
|
|
34
|
+
}
|
|
35
|
+
return items
|
|
36
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request and response shapes of the v1 API, as they arrive over JSON.
|
|
3
|
+
*
|
|
4
|
+
* The routes define these with zod in `apps/web` (`lib/api/contracts.ts` and
|
|
5
|
+
* the route files). The SDK is published without dependencies, so it cannot
|
|
6
|
+
* import those schemas; instead each response type has a field list below that
|
|
7
|
+
* the compiler checks against the type, and `apps/web/lib/api/sdk-contract.test.ts`
|
|
8
|
+
* checks against the route's zod schema. A field added on one side and not the
|
|
9
|
+
* other fails that test rather than reaching a user.
|
|
10
|
+
*
|
|
11
|
+
* Dates are ISO strings: that is what JSON carries.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none'
|
|
15
|
+
export const PRIORITIES: readonly Priority[] = ['urgent', 'high', 'medium', 'low', 'none']
|
|
16
|
+
|
|
17
|
+
export type StateGroup = 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled' | 'triage'
|
|
18
|
+
|
|
19
|
+
export interface WorkItem {
|
|
20
|
+
id: string
|
|
21
|
+
key: string
|
|
22
|
+
sequenceId: number
|
|
23
|
+
name: string
|
|
24
|
+
priority: Priority
|
|
25
|
+
stateId: string | null
|
|
26
|
+
assigneeIds: string[]
|
|
27
|
+
labelIds: string[]
|
|
28
|
+
parentId: string | null
|
|
29
|
+
sortOrder: string
|
|
30
|
+
createdAt: string
|
|
31
|
+
/**
|
|
32
|
+
* Returned by `GET /work-items/{id}` (always, from Slice 2A on); not yet by
|
|
33
|
+
* lists or writes.
|
|
34
|
+
*/
|
|
35
|
+
projectId?: string
|
|
36
|
+
/** Not returned yet. Sent back as `If-Match` once the server versions items. */
|
|
37
|
+
version?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkItemDetail extends WorkItem {
|
|
41
|
+
/** Optional only so the SDK still reads a server older than Slice 2A. */
|
|
42
|
+
projectId?: string
|
|
43
|
+
description: string | null
|
|
44
|
+
startDate: string | null
|
|
45
|
+
targetDate: string | null
|
|
46
|
+
archivedAt: string | null
|
|
47
|
+
updatedAt: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Page<T> {
|
|
51
|
+
items: T[]
|
|
52
|
+
nextCursor: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CreateWorkItemInput {
|
|
56
|
+
name: string
|
|
57
|
+
description?: string
|
|
58
|
+
stateId?: string
|
|
59
|
+
priority?: Priority
|
|
60
|
+
assigneeIds?: string[]
|
|
61
|
+
labelIds?: string[]
|
|
62
|
+
parentId?: string
|
|
63
|
+
/** ISO date. */
|
|
64
|
+
startDate?: string
|
|
65
|
+
targetDate?: string
|
|
66
|
+
/** One of the project's work item types; absent means the project's default. */
|
|
67
|
+
typeId?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The fields PATCH accepts today. The server strips anything else, so sending
|
|
72
|
+
* an unlisted field would look like success and change nothing.
|
|
73
|
+
*/
|
|
74
|
+
export interface UpdateWorkItemInput {
|
|
75
|
+
name?: string
|
|
76
|
+
description?: string
|
|
77
|
+
stateId?: string
|
|
78
|
+
priority?: Priority
|
|
79
|
+
parentId?: string | null
|
|
80
|
+
startDate?: string | null
|
|
81
|
+
targetDate?: string | null
|
|
82
|
+
/** A work item type of the item's project; null clears it. */
|
|
83
|
+
typeId?: string | null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ListWorkItemsOptions {
|
|
87
|
+
/** A @devpulse/query expression; sent JSON-encoded as `filter`. */
|
|
88
|
+
filter?: unknown
|
|
89
|
+
limit?: number
|
|
90
|
+
cursor?: string
|
|
91
|
+
/** A state id. */
|
|
92
|
+
state?: string
|
|
93
|
+
priority?: Priority
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface State {
|
|
97
|
+
id: string
|
|
98
|
+
name: string
|
|
99
|
+
group: StateGroup | string
|
|
100
|
+
color: string
|
|
101
|
+
description: string | null
|
|
102
|
+
/** Fractional-index sort key; the list arrives already ordered by it. */
|
|
103
|
+
sequence: string
|
|
104
|
+
isDefault: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface Label {
|
|
108
|
+
id: string
|
|
109
|
+
name: string
|
|
110
|
+
color: string
|
|
111
|
+
description: string | null
|
|
112
|
+
sortOrder: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface Cycle {
|
|
116
|
+
id: string
|
|
117
|
+
name: string
|
|
118
|
+
description: string | null
|
|
119
|
+
startDate: string | null
|
|
120
|
+
endDate: string | null
|
|
121
|
+
status: 'draft' | 'upcoming' | 'active' | 'completed'
|
|
122
|
+
sortOrder: string
|
|
123
|
+
completedAt: string | null
|
|
124
|
+
archivedAt: string | null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CycleProgress {
|
|
128
|
+
progress: unknown
|
|
129
|
+
burndown: unknown
|
|
130
|
+
frozen: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface View {
|
|
134
|
+
id: string
|
|
135
|
+
name: string
|
|
136
|
+
description: string | null
|
|
137
|
+
filter: unknown
|
|
138
|
+
display: unknown
|
|
139
|
+
access: 'private' | 'public'
|
|
140
|
+
ownerId: string | null
|
|
141
|
+
sortOrder: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CreateViewInput {
|
|
145
|
+
name: string
|
|
146
|
+
description?: string
|
|
147
|
+
where?: unknown
|
|
148
|
+
display?: Record<string, unknown>
|
|
149
|
+
access?: 'private' | 'public'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type IntakeStatus = 'pending' | 'accepted' | 'declined' | 'snoozed' | 'duplicate'
|
|
153
|
+
|
|
154
|
+
export interface IntakeItem {
|
|
155
|
+
id: string
|
|
156
|
+
workItemId: string
|
|
157
|
+
status: IntakeStatus
|
|
158
|
+
source: string
|
|
159
|
+
sourceEmail: string | null
|
|
160
|
+
snoozedUntil: string | null
|
|
161
|
+
submittedAt: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface SubmitIntakeInput {
|
|
165
|
+
title: string
|
|
166
|
+
description?: string
|
|
167
|
+
source?: 'in_app' | 'form' | 'email' | 'api' | 'integration'
|
|
168
|
+
sourceEmail?: string
|
|
169
|
+
extra?: Record<string, unknown>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SubmitIntakeResult {
|
|
173
|
+
intakeItemId: string
|
|
174
|
+
workItemId: string
|
|
175
|
+
sequenceId: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface TriageInput {
|
|
179
|
+
status: 'accepted' | 'declined' | 'snoozed' | 'duplicate'
|
|
180
|
+
/** ISO date; required when snoozing. */
|
|
181
|
+
snoozedUntil?: string
|
|
182
|
+
/** Required when marking a duplicate. */
|
|
183
|
+
duplicateOfId?: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface TriageResult {
|
|
187
|
+
status: string
|
|
188
|
+
movedToStateId: string | null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface ImportPreview {
|
|
192
|
+
source: string
|
|
193
|
+
total: number
|
|
194
|
+
states: { name: string; suggestedStateId: string | null }[]
|
|
195
|
+
labels: { name: string; exists: boolean }[]
|
|
196
|
+
assignees: { value: string; matched: boolean }[]
|
|
197
|
+
skipped: { row: number; reason: string }[]
|
|
198
|
+
unmappedPriorities: string[]
|
|
199
|
+
/**
|
|
200
|
+
* False when re-importing would create duplicates rather than update — a CSV
|
|
201
|
+
* with no id column. Worth showing before the import, not after.
|
|
202
|
+
*/
|
|
203
|
+
idempotent: boolean
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ImportRun {
|
|
207
|
+
id: string
|
|
208
|
+
source: string
|
|
209
|
+
status: string
|
|
210
|
+
totalItems: number
|
|
211
|
+
createdCount: number
|
|
212
|
+
updatedCount: number
|
|
213
|
+
cursor: number
|
|
214
|
+
skipped: { row: number; reason: string }[]
|
|
215
|
+
unmatched: { kind: string; value: string; item: string }[]
|
|
216
|
+
error: string | null
|
|
217
|
+
startedAt?: string | null
|
|
218
|
+
finishedAt?: string | null
|
|
219
|
+
createdAt?: string
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type ImportSource = 'csv' | 'github' | 'jira' | 'linear'
|
|
223
|
+
|
|
224
|
+
export interface ImportInput {
|
|
225
|
+
source: ImportSource
|
|
226
|
+
/** CSV text, or the parsed JSON export for the API sources. */
|
|
227
|
+
data: unknown
|
|
228
|
+
repository?: string
|
|
229
|
+
baseUrl?: string
|
|
230
|
+
/** Source state name to state id. Only for `startImport`. */
|
|
231
|
+
stateMap?: Record<string, string>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface Health {
|
|
235
|
+
status: string
|
|
236
|
+
checks: Record<string, string>
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `GET /api/v1/me`: who the credential is. Never includes the key. */
|
|
240
|
+
export interface Me {
|
|
241
|
+
principal: {
|
|
242
|
+
kind: 'personal_key' | 'service_key' | string
|
|
243
|
+
scopeType?: 'project' | 'organization' | 'personal' | string
|
|
244
|
+
apiKeyId: string
|
|
245
|
+
keyPrefix: string
|
|
246
|
+
userId: string | null
|
|
247
|
+
email: string | null
|
|
248
|
+
organizationRole?: string | null
|
|
249
|
+
}
|
|
250
|
+
organization: { id: string; slug: string; name: string }
|
|
251
|
+
project: { id: string; identifier: string; name: string } | null
|
|
252
|
+
scopes: string[]
|
|
253
|
+
expiresAt: string | null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `GET /api/v1/projects`. */
|
|
257
|
+
export interface Project {
|
|
258
|
+
id: string
|
|
259
|
+
identifier: string
|
|
260
|
+
name: string
|
|
261
|
+
archivedAt: string | null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Field lists, checked by the compiler to name every key of the type (and no
|
|
266
|
+
* others), and by the contract test to match the route's response schema.
|
|
267
|
+
*/
|
|
268
|
+
type FieldsOf<T> = { [K in keyof Required<T>]: true }
|
|
269
|
+
|
|
270
|
+
export const WORK_ITEM_FIELDS: FieldsOf<WorkItem> = {
|
|
271
|
+
id: true,
|
|
272
|
+
key: true,
|
|
273
|
+
sequenceId: true,
|
|
274
|
+
name: true,
|
|
275
|
+
priority: true,
|
|
276
|
+
stateId: true,
|
|
277
|
+
assigneeIds: true,
|
|
278
|
+
labelIds: true,
|
|
279
|
+
parentId: true,
|
|
280
|
+
sortOrder: true,
|
|
281
|
+
createdAt: true,
|
|
282
|
+
projectId: true,
|
|
283
|
+
version: true,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const WORK_ITEM_DETAIL_FIELDS: FieldsOf<WorkItemDetail> = {
|
|
287
|
+
...WORK_ITEM_FIELDS,
|
|
288
|
+
description: true,
|
|
289
|
+
startDate: true,
|
|
290
|
+
targetDate: true,
|
|
291
|
+
archivedAt: true,
|
|
292
|
+
updatedAt: true,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export const STATE_FIELDS: FieldsOf<State> = {
|
|
296
|
+
id: true,
|
|
297
|
+
name: true,
|
|
298
|
+
group: true,
|
|
299
|
+
color: true,
|
|
300
|
+
description: true,
|
|
301
|
+
sequence: true,
|
|
302
|
+
isDefault: true,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const LABEL_FIELDS: FieldsOf<Label> = {
|
|
306
|
+
id: true,
|
|
307
|
+
name: true,
|
|
308
|
+
color: true,
|
|
309
|
+
description: true,
|
|
310
|
+
sortOrder: true,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export const CYCLE_FIELDS: FieldsOf<Cycle> = {
|
|
314
|
+
id: true,
|
|
315
|
+
name: true,
|
|
316
|
+
description: true,
|
|
317
|
+
startDate: true,
|
|
318
|
+
endDate: true,
|
|
319
|
+
status: true,
|
|
320
|
+
sortOrder: true,
|
|
321
|
+
completedAt: true,
|
|
322
|
+
archivedAt: true,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export const VIEW_FIELDS: FieldsOf<View> = {
|
|
326
|
+
id: true,
|
|
327
|
+
name: true,
|
|
328
|
+
description: true,
|
|
329
|
+
filter: true,
|
|
330
|
+
display: true,
|
|
331
|
+
access: true,
|
|
332
|
+
ownerId: true,
|
|
333
|
+
sortOrder: true,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const INTAKE_ITEM_FIELDS: FieldsOf<IntakeItem> = {
|
|
337
|
+
id: true,
|
|
338
|
+
workItemId: true,
|
|
339
|
+
status: true,
|
|
340
|
+
source: true,
|
|
341
|
+
sourceEmail: true,
|
|
342
|
+
snoozedUntil: true,
|
|
343
|
+
submittedAt: true,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const IMPORT_RUN_FIELDS: FieldsOf<ImportRun> = {
|
|
347
|
+
id: true,
|
|
348
|
+
source: true,
|
|
349
|
+
status: true,
|
|
350
|
+
totalItems: true,
|
|
351
|
+
createdCount: true,
|
|
352
|
+
updatedCount: true,
|
|
353
|
+
cursor: true,
|
|
354
|
+
skipped: true,
|
|
355
|
+
unmatched: true,
|
|
356
|
+
error: true,
|
|
357
|
+
startedAt: true,
|
|
358
|
+
finishedAt: true,
|
|
359
|
+
createdAt: true,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fields the SDK declares ahead of the server (the work management contracts
|
|
364
|
+
* add them). The contract test allows them to be missing from today's schemas.
|
|
365
|
+
*/
|
|
366
|
+
export const PENDING_FIELDS: Record<string, readonly string[]> = {
|
|
367
|
+
workItem: ['projectId', 'version'],
|
|
368
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published version, for `User-Agent`.
|
|
3
|
+
*
|
|
4
|
+
* Replaced at build time (`bun build --define`) from package.json, so the
|
|
5
|
+
* Version PR that bumps package.json needs no second edit. Unbuilt source —
|
|
6
|
+
* tests, the workspace — reports `0.0.0-dev`.
|
|
7
|
+
*/
|
|
8
|
+
declare const __YCADENCE_SDK_VERSION__: string | undefined
|
|
9
|
+
|
|
10
|
+
export const SDK_VERSION: string =
|
|
11
|
+
typeof __YCADENCE_SDK_VERSION__ === 'string' ? __YCADENCE_SDK_VERSION__ : '0.0.0-dev'
|