@knpkv/jira-cli 1.2.3 → 1.4.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/CHANGELOG.md +133 -0
- package/README.md +64 -0
- package/dist/AttachmentService.d.ts +2 -2
- package/dist/AttachmentService.d.ts.map +1 -1
- package/dist/AttachmentService.js +1 -1
- package/dist/AttachmentService.js.map +1 -1
- package/dist/IssueService.d.ts +82 -3
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +111 -16
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +3 -3
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +87 -23
- package/dist/JiraAuth.js.map +1 -1
- package/dist/MarkdownWriter.d.ts +4 -4
- package/dist/MarkdownWriter.d.ts.map +1 -1
- package/dist/MarkdownWriter.js +2 -2
- package/dist/MarkdownWriter.js.map +1 -1
- package/dist/SyncWorkspace.d.ts +2 -2
- package/dist/SyncWorkspace.d.ts.map +1 -1
- package/dist/SyncWorkspace.js.map +1 -1
- package/dist/VersionService.d.ts +81 -9
- package/dist/VersionService.d.ts.map +1 -1
- package/dist/VersionService.js +113 -17
- package/dist/VersionService.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +13 -2
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +7 -7
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/edit.d.ts +22 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +49 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/errorHandler.d.ts.map +1 -1
- package/dist/commands/get.d.ts +6 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +3 -3
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/issue.d.ts +2 -2
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +3 -2
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/issueAttachment.d.ts +1 -1
- package/dist/commands/issueAttachment.d.ts.map +1 -1
- package/dist/commands/issueAttachment.js +13 -11
- package/dist/commands/issueAttachment.js.map +1 -1
- package/dist/commands/layers.d.ts +3 -3
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +5 -2
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.d.ts +6 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +7 -7
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/version.d.ts +17 -2
- package/dist/commands/version.d.ts.map +1 -1
- package/dist/commands/version.js +131 -19
- package/dist/commands/version.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.js +1 -1
- package/dist/internal/NodeLayers.js.map +1 -1
- package/dist/internal/attachmentInsertion.d.ts.map +1 -1
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +2 -1
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/jqlBuilder.d.ts.map +1 -1
- package/dist/internal/oauthServer.d.ts +10 -4
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +13 -8
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/openBrowser.d.ts +1 -1
- package/dist/internal/openBrowser.d.ts.map +1 -1
- package/dist/internal/openBrowser.js +1 -1
- package/dist/internal/openBrowser.js.map +1 -1
- package/dist/internal/sync/baseline.d.ts.map +1 -1
- package/dist/internal/sync/baseline.js +2 -2
- package/dist/internal/sync/baseline.js.map +1 -1
- package/dist/internal/sync/changes.d.ts.map +1 -1
- package/dist/internal/sync/config.d.ts.map +1 -1
- package/dist/internal/sync/config.js +1 -1
- package/dist/internal/sync/config.js.map +1 -1
- package/dist/internal/sync/document.d.ts.map +1 -1
- package/dist/internal/sync/document.js +8 -8
- package/dist/internal/sync/document.js.map +1 -1
- package/dist/internal/sync/fieldValues.d.ts +9 -9
- package/dist/internal/sync/fieldValues.d.ts.map +1 -1
- package/dist/internal/sync/fieldValues.js +12 -16
- package/dist/internal/sync/fieldValues.js.map +1 -1
- package/dist/internal/sync/manifest.d.ts.map +1 -1
- package/dist/internal/sync/manifest.js +1 -1
- package/dist/internal/sync/manifest.js.map +1 -1
- package/dist/internal/sync/paths.d.ts.map +1 -1
- package/dist/internal/sync/schemas.d.ts +17 -9
- package/dist/internal/sync/schemas.d.ts.map +1 -1
- package/dist/internal/sync/schemas.js +6 -6
- package/dist/internal/sync/schemas.js.map +1 -1
- package/dist/internal/sync/types.d.ts +4 -4
- package/dist/internal/sync/types.d.ts.map +1 -1
- package/dist/internal/sync/types.js +1 -1
- package/dist/internal/sync/types.js.map +1 -1
- package/package.json +15 -11
- package/skills/jira/SKILL.md +17 -1
- package/src/AttachmentService.ts +6 -6
- package/src/IssueService.ts +214 -27
- package/src/JiraAuth.ts +106 -42
- package/src/MarkdownWriter.ts +6 -6
- package/src/SyncWorkspace.ts +12 -11
- package/src/VersionService.ts +199 -27
- package/src/bin.ts +1 -1
- package/src/commands/auth.ts +7 -7
- package/src/commands/edit.ts +88 -0
- package/src/commands/get.ts +3 -3
- package/src/commands/issue.ts +3 -2
- package/src/commands/issueAttachment.ts +21 -20
- package/src/commands/layers.ts +5 -2
- package/src/commands/search.ts +7 -7
- package/src/commands/version.ts +176 -23
- package/src/index.ts +13 -3
- package/src/internal/NodeLayers.ts +1 -1
- package/src/internal/frontmatter.ts +3 -2
- package/src/internal/oauthServer.ts +29 -16
- package/src/internal/openBrowser.ts +1 -1
- package/src/internal/sync/baseline.ts +3 -2
- package/src/internal/sync/config.ts +2 -1
- package/src/internal/sync/document.ts +15 -14
- package/src/internal/sync/fieldValues.ts +21 -22
- package/src/internal/sync/manifest.ts +1 -1
- package/src/internal/sync/schemas.ts +6 -6
- package/src/internal/sync/types.ts +4 -4
- package/test/IssueService.test.ts +2 -2
- package/test/JiraAuthRefresh.test.ts +269 -0
- package/test/VersionService.test.ts +2 -2
- package/test/commandTree.test.ts +158 -2
- package/test/editIssuePayload.test.ts +103 -0
- package/test/frontmatter.test.ts +4 -2
- package/test/integration.test.ts +12 -10
- package/test/oauthServer.test.ts +77 -4
- package/test/publicApi.test.ts +34 -0
- package/test/relatedWorkSync.test.ts +179 -0
- package/test/remoteWrites.test.ts +220 -0
- package/test/syncChanges.test.ts +2 -2
- package/test/syncConfig.test.ts +2 -2
|
@@ -14,11 +14,12 @@ import { OAuthError } from "@knpkv/atlassian-common/auth"
|
|
|
14
14
|
import * as Context from "effect/Context"
|
|
15
15
|
import * as Deferred from "effect/Deferred"
|
|
16
16
|
import * as Effect from "effect/Effect"
|
|
17
|
+
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http"
|
|
18
|
+
import type * as HttpServerError from "effect/http/HttpServerError"
|
|
17
19
|
import * as Layer from "effect/Layer"
|
|
20
|
+
import { NetAddress } from "effect/net"
|
|
18
21
|
import * as Schema from "effect/Schema"
|
|
19
22
|
import type * as Scope from "effect/Scope"
|
|
20
|
-
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
|
|
21
|
-
import type * as HttpServerError from "effect/unstable/http/HttpServerError"
|
|
22
23
|
|
|
23
24
|
const DEFAULT_PORT = 8585
|
|
24
25
|
const MAX_PORT = 8594
|
|
@@ -30,13 +31,25 @@ type HttpServerInstance = Effect.Success<typeof HttpServer.HttpServer>
|
|
|
30
31
|
* @category Services
|
|
31
32
|
*/
|
|
32
33
|
export interface HttpServerFactory {
|
|
33
|
-
readonly createServerLayer: (
|
|
34
|
+
readonly createServerLayer: (options: CallbackServerListenOptions) => Layer.Layer<
|
|
34
35
|
HttpServer.HttpServer,
|
|
35
36
|
HttpServerError.ServeError,
|
|
36
37
|
never
|
|
37
38
|
>
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export interface CallbackServerListenOptions {
|
|
42
|
+
readonly host: "localhost"
|
|
43
|
+
readonly port: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const callbackServerListenOptions = (port: number): CallbackServerListenOptions => ({
|
|
47
|
+
host: "localhost",
|
|
48
|
+
port
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const callbackUrl = (port: number): string => `http://localhost:${port}/callback`
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Tag for the HttpServerFactory service.
|
|
42
55
|
*
|
|
@@ -57,7 +70,9 @@ export class HttpServerFactoryTag extends Context.Service<
|
|
|
57
70
|
* @category Layers
|
|
58
71
|
*/
|
|
59
72
|
export const makeHttpServerFactory = (
|
|
60
|
-
createLayerFn: (
|
|
73
|
+
createLayerFn: (
|
|
74
|
+
options: CallbackServerListenOptions
|
|
75
|
+
) => Layer.Layer<HttpServer.HttpServer, HttpServerError.ServeError, never>
|
|
61
76
|
): Layer.Layer<HttpServerFactoryTag> =>
|
|
62
77
|
Layer.succeed(HttpServerFactoryTag, {
|
|
63
78
|
createServerLayer: createLayerFn
|
|
@@ -98,7 +113,7 @@ export const startCallbackServer = (
|
|
|
98
113
|
{ readonly context: Context.Context<HttpServer.HttpServer>; readonly port: number },
|
|
99
114
|
OAuthError
|
|
100
115
|
> =>
|
|
101
|
-
Layer.buildWithScope(factory.createServerLayer(port), serverScope).pipe(
|
|
116
|
+
Layer.buildWithScope(factory.createServerLayer(callbackServerListenOptions(port)), serverScope).pipe(
|
|
102
117
|
Effect.map((context) => ({ context, port })),
|
|
103
118
|
Effect.catchIf(
|
|
104
119
|
(error) => isAddressInUse(error) && port < MAX_PORT,
|
|
@@ -108,7 +123,7 @@ export const startCallbackServer = (
|
|
|
108
123
|
)
|
|
109
124
|
const { context: serverContext } = yield* buildServerContext(DEFAULT_PORT)
|
|
110
125
|
const server: HttpServerInstance = Context.get(serverContext, HttpServer.HttpServer)
|
|
111
|
-
const port = yield* (server.address
|
|
126
|
+
const port = yield* (NetAddress.isInetAddress(server.address)
|
|
112
127
|
? Effect.succeed(server.address.port)
|
|
113
128
|
: Effect.fail(new OAuthError({ step: "authorize", cause: "OAuth callback server must listen on a TCP port" })))
|
|
114
129
|
|
|
@@ -118,29 +133,27 @@ export const startCallbackServer = (
|
|
|
118
133
|
"/callback",
|
|
119
134
|
(req) =>
|
|
120
135
|
Effect.gen(function*() {
|
|
121
|
-
const url = new URL(req.url,
|
|
136
|
+
const url = new URL(req.url, callbackUrl(port))
|
|
122
137
|
const code = url.searchParams.get("code")
|
|
123
138
|
const state = url.searchParams.get("state")
|
|
124
139
|
const error = url.searchParams.get("error")
|
|
125
140
|
const errorDescription = url.searchParams.get("error_description")
|
|
126
141
|
|
|
127
|
-
if (
|
|
128
|
-
yield* Deferred.fail(
|
|
129
|
-
deferred,
|
|
130
|
-
new OAuthError({ step: "authorize", cause: errorDescription ?? error })
|
|
131
|
-
)
|
|
142
|
+
if (state !== expectedState) {
|
|
132
143
|
return HttpServerResponse.html(
|
|
133
|
-
"<html><body><h1>
|
|
144
|
+
"<html><body><h1>Security Error</h1><p>State verification failed.</p></body></html>"
|
|
145
|
+
).pipe(
|
|
146
|
+
HttpServerResponse.setStatus(403)
|
|
134
147
|
)
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
if (
|
|
150
|
+
if (error) {
|
|
138
151
|
yield* Deferred.fail(
|
|
139
152
|
deferred,
|
|
140
|
-
new OAuthError({ step: "authorize", cause:
|
|
153
|
+
new OAuthError({ step: "authorize", cause: errorDescription ?? error })
|
|
141
154
|
)
|
|
142
155
|
return HttpServerResponse.html(
|
|
143
|
-
"<html><body><h1>
|
|
156
|
+
"<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
|
|
144
157
|
)
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Effect from "effect/Effect"
|
|
7
7
|
import type * as PlatformError from "effect/PlatformError"
|
|
8
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
8
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
9
9
|
|
|
10
10
|
const run = (
|
|
11
11
|
command: string,
|
|
@@ -15,7 +15,7 @@ export const parseSyncBaseline = (
|
|
|
15
15
|
): Effect.Effect<SyncBaseline, SyncWorkspaceError | SyncValidationError> =>
|
|
16
16
|
Effect.gen(function*() {
|
|
17
17
|
const raw = yield* Effect.try({
|
|
18
|
-
try: ()
|
|
18
|
+
try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(content),
|
|
19
19
|
catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Baseline JSON", path, cause })
|
|
20
20
|
})
|
|
21
21
|
const baseline: SyncBaseline = yield* Schema.decodeUnknownEffect(SyncBaselineSchema)(raw).pipe(
|
|
@@ -24,4 +24,5 @@ export const parseSyncBaseline = (
|
|
|
24
24
|
return baseline
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
export const serializeSyncBaseline = (baseline: SyncBaseline): string =>
|
|
27
|
+
export const serializeSyncBaseline = (baseline: SyncBaseline): string =>
|
|
28
|
+
`${JSON.stringify(Schema.encodeSync(SyncBaselineSchema)(baseline), null, 2)}\n`
|
|
@@ -28,7 +28,8 @@ export const parseWorkspaceConfig = (
|
|
|
28
28
|
return config
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
export const serializeWorkspaceConfig = (config: WorkspaceConfig): string =>
|
|
31
|
+
export const serializeWorkspaceConfig = (config: WorkspaceConfig): string =>
|
|
32
|
+
yaml.dump(Schema.encodeSync(WorkspaceConfigSchema)(config), { lineWidth: 100 })
|
|
32
33
|
|
|
33
34
|
export const makeDefaultWorkspaceConfig = (siteUrl: string): WorkspaceConfig => ({
|
|
34
35
|
version: 1,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { isPreviewableAttachment } from "@knpkv/atlassian-common/attachments"
|
|
7
7
|
import * as Predicate from "effect/Predicate"
|
|
8
|
+
import type * as Schema from "effect/Schema"
|
|
8
9
|
import matter from "gray-matter"
|
|
9
10
|
import * as yaml from "js-yaml"
|
|
10
11
|
import { SyncValidationError } from "../../JiraCliError.js"
|
|
@@ -17,19 +18,19 @@ import type {
|
|
|
17
18
|
UserFieldValue
|
|
18
19
|
} from "./types.js"
|
|
19
20
|
|
|
20
|
-
const isStringArray = (value:
|
|
21
|
-
Array.isArray(value) && value.every((item) =>
|
|
21
|
+
const isStringArray = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & ReadonlyArray<string> =>
|
|
22
|
+
Array.isArray(value) && value.every((item) => Predicate.isString(item))
|
|
22
23
|
|
|
23
|
-
const isFrontMatterCustomFields = (
|
|
24
|
-
value:
|
|
25
|
-
): value is IssueDocumentFrontMatter["customFields"] => Predicate.isReadonlyObject(value)
|
|
24
|
+
const isFrontMatterCustomFields = <UnparsedInput>(
|
|
25
|
+
value: UnparsedInput
|
|
26
|
+
): value is UnparsedInput & IssueDocumentFrontMatter["customFields"] => Predicate.isReadonlyObject(value)
|
|
26
27
|
|
|
27
28
|
const yamlEngine = {
|
|
28
29
|
parse: (str: string): object => {
|
|
29
30
|
const value = yaml.load(str)
|
|
30
31
|
return Predicate.isReadonlyObject(value) ? value : {}
|
|
31
32
|
},
|
|
32
|
-
stringify: (data:
|
|
33
|
+
stringify: <Data extends object>(data: Data): string => yaml.dump(data)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export const DESCRIPTION_SECTION = "Description"
|
|
@@ -88,17 +89,17 @@ const section = (name: string, content: string): string => {
|
|
|
88
89
|
return normalized.length > 0 ? `## ${name}\n\n${normalized}\n` : `## ${name}\n`
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
const parseFrontMatter = (path: string, data: Record<string,
|
|
92
|
+
const parseFrontMatter = (path: string, data: Record<string, Schema.Json>): IssueDocumentFrontMatter => {
|
|
92
93
|
const requiredString = (key: string): string => {
|
|
93
94
|
const value = data[key]
|
|
94
|
-
if (
|
|
95
|
+
if (Predicate.isString(value)) return value
|
|
95
96
|
return fail(path, `Missing or invalid front matter field "${key}"`)
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
const nullableString = (key: string): string | null => {
|
|
99
100
|
const value = data[key]
|
|
100
101
|
if (value === null || value === undefined) return null
|
|
101
|
-
if (
|
|
102
|
+
if (Predicate.isString(value)) return value
|
|
102
103
|
return fail(path, `Invalid front matter field "${key}"`)
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -108,7 +109,7 @@ const parseFrontMatter = (path: string, data: Record<string, unknown>): IssueDoc
|
|
|
108
109
|
const record = Predicate.isReadonlyObject(value) ? value : fail(path, `Invalid user field "${key}"`)
|
|
109
110
|
const accountId = record["accountId"]
|
|
110
111
|
const displayName = record["displayName"]
|
|
111
|
-
if (
|
|
112
|
+
if (Predicate.isString(accountId) && Predicate.isString(displayName)) {
|
|
112
113
|
return {
|
|
113
114
|
accountId,
|
|
114
115
|
displayName
|
|
@@ -244,13 +245,13 @@ const parseAttachmentMetadata = (
|
|
|
244
245
|
const parsed = JSON.parse(raw)
|
|
245
246
|
if (!Predicate.isReadonlyObject(parsed)) return {}
|
|
246
247
|
return {
|
|
247
|
-
id:
|
|
248
|
+
id: Predicate.isString(parsed["jiraAttachmentId"])
|
|
248
249
|
? parsed["jiraAttachmentId"]
|
|
249
|
-
:
|
|
250
|
+
: Predicate.isString(parsed["id"])
|
|
250
251
|
? parsed["id"]
|
|
251
252
|
: "",
|
|
252
|
-
mediaType:
|
|
253
|
-
size:
|
|
253
|
+
mediaType: Predicate.isString(parsed["mediaType"]) ? parsed["mediaType"] : null,
|
|
254
|
+
size: Predicate.isNumber(parsed["size"]) ? parsed["size"] : null
|
|
254
255
|
}
|
|
255
256
|
} catch {
|
|
256
257
|
return {}
|
|
@@ -16,7 +16,7 @@ export interface CanonicalFieldValueOptions {
|
|
|
16
16
|
|
|
17
17
|
export const explicitClear = null
|
|
18
18
|
|
|
19
|
-
export const isExplicitClear = (value:
|
|
19
|
+
export const isExplicitClear = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & null => value === null
|
|
20
20
|
|
|
21
21
|
export const userFieldValue = (accountId: string, displayName: string): UserFieldValue => ({
|
|
22
22
|
accountId,
|
|
@@ -44,24 +44,26 @@ export const completeListValue = (
|
|
|
44
44
|
|
|
45
45
|
export const makeCompleteListValue = completeListValue
|
|
46
46
|
|
|
47
|
-
export const isUserFieldValue = (value:
|
|
47
|
+
export const isUserFieldValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & UserFieldValue => {
|
|
48
48
|
if (!Predicate.isReadonlyObject(value)) return false
|
|
49
|
-
return
|
|
49
|
+
return Predicate.isString(value["accountId"]) && Predicate.isString(value["displayName"])
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const isOptionFieldValue = (value:
|
|
53
|
-
if (!Predicate.isReadonlyObject(value) ||
|
|
52
|
+
export const isOptionFieldValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & OptionFieldValue => {
|
|
53
|
+
if (!Predicate.isReadonlyObject(value) || !Predicate.isString(value["value"])) return false
|
|
54
54
|
const id = value["id"]
|
|
55
|
-
return id === undefined ||
|
|
55
|
+
return id === undefined || Predicate.isString(id)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export const isCascadingFieldValue = (
|
|
58
|
+
export const isCascadingFieldValue = <UnparsedInput>(
|
|
59
|
+
value: UnparsedInput
|
|
60
|
+
): value is UnparsedInput & CascadingFieldValue => {
|
|
59
61
|
if (!Predicate.isReadonlyObject(value) || !isOptionFieldValue(value["parent"])) return false
|
|
60
62
|
const child = value["child"]
|
|
61
63
|
return child === undefined || isOptionFieldValue(child)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
export const isCompleteListValue = (value:
|
|
66
|
+
export const isCompleteListValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & CompleteListValue =>
|
|
65
67
|
Array.isArray(value) && value.every(isCompleteListItem)
|
|
66
68
|
|
|
67
69
|
export const canonicalFieldOrder = <A extends CompleteListItem>(
|
|
@@ -104,7 +106,7 @@ const canonicalFieldValueKey = (
|
|
|
104
106
|
options: CanonicalFieldValueOptions
|
|
105
107
|
): string => {
|
|
106
108
|
if (value === null) return "clear:"
|
|
107
|
-
if (
|
|
109
|
+
if (isCompleteListValue(value)) {
|
|
108
110
|
return `list:${canonicalFieldOrder(value, options).map(completeListItemOrderKey).join("\u0000")}`
|
|
109
111
|
}
|
|
110
112
|
if (isCascadingFieldValue(value)) {
|
|
@@ -114,13 +116,13 @@ const canonicalFieldValueKey = (
|
|
|
114
116
|
}
|
|
115
117
|
if (isUserFieldValue(value)) return userOrderKey(value)
|
|
116
118
|
if (isOptionFieldValue(value)) return optionOrderKey(value)
|
|
117
|
-
return
|
|
119
|
+
return scalarOrderKey(value)
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
const completeListItemOrderKey = (value: CompleteListItem): string => {
|
|
121
123
|
if (isUserFieldValue(value)) return userOrderKey(value)
|
|
122
124
|
if (isOptionFieldValue(value)) return optionOrderKey(value)
|
|
123
|
-
return
|
|
125
|
+
return scalarOrderKey(value)
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
const userOrderKey = (value: UserFieldValue): string =>
|
|
@@ -129,15 +131,12 @@ const userOrderKey = (value: UserFieldValue): string =>
|
|
|
129
131
|
const optionOrderKey = (value: OptionFieldValue): string =>
|
|
130
132
|
`option:${value.value.toLocaleLowerCase("en")}\u0000${value.id ?? ""}`
|
|
131
133
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
default:
|
|
141
|
-
return false
|
|
142
|
-
}
|
|
134
|
+
const scalarOrderKey = (value: string | number | boolean): string => {
|
|
135
|
+
const kind = Predicate.isString(value) ? "string" : Predicate.isNumber(value) ? "number" : "boolean"
|
|
136
|
+
return `${kind}:${String(value)}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isCompleteListItem = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & CompleteListItem => {
|
|
140
|
+
return Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value) ||
|
|
141
|
+
isUserFieldValue(value) || isOptionFieldValue(value)
|
|
143
142
|
}
|
|
@@ -21,7 +21,7 @@ export const parseSyncManifest = (
|
|
|
21
21
|
): Effect.Effect<SyncManifest, SyncWorkspaceError | SyncValidationError> =>
|
|
22
22
|
Effect.gen(function*() {
|
|
23
23
|
const raw = yield* Effect.try({
|
|
24
|
-
try: ()
|
|
24
|
+
try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(content),
|
|
25
25
|
catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Manifest JSON", path, cause })
|
|
26
26
|
})
|
|
27
27
|
return yield* Schema.decodeUnknownEffect(SyncManifestSchema)(raw).pipe(
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Effect from "effect/Effect"
|
|
7
7
|
import * as Schema from "effect/Schema"
|
|
8
|
-
import {
|
|
8
|
+
import { FIELD_CONTRACTS } from "./types.js"
|
|
9
9
|
|
|
10
10
|
const SiteUrl = Schema.String.pipe(
|
|
11
11
|
Schema.check(Schema.isPattern(/^https:\/\/[a-z0-9][a-z0-9-]*\.atlassian\.net$/))
|
|
@@ -14,14 +14,14 @@ const SiteUrl = Schema.String.pipe(
|
|
|
14
14
|
const NonEmptyString = Schema.String.pipe(Schema.check(Schema.isMinLength(1)))
|
|
15
15
|
const FilenameModes: readonly ["convention", "custom"] = ["convention", "custom"]
|
|
16
16
|
|
|
17
|
-
export const
|
|
17
|
+
export const FieldContractSchema = Schema.Literals(FIELD_CONTRACTS)
|
|
18
18
|
|
|
19
19
|
export const RequestedCustomFieldSchema = Schema.Struct({
|
|
20
20
|
displayName: NonEmptyString,
|
|
21
21
|
fieldId: Schema.optional(NonEmptyString),
|
|
22
|
-
|
|
22
|
+
form: FieldContractSchema,
|
|
23
23
|
ordered: Schema.optional(Schema.Boolean)
|
|
24
|
-
})
|
|
24
|
+
}).pipe(Schema.encodeKeys({ form: "shape" }))
|
|
25
25
|
|
|
26
26
|
export const WorkspaceConfigSchema = Schema.Struct({
|
|
27
27
|
version: Schema.Literal(1).pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1))),
|
|
@@ -82,9 +82,9 @@ export const SyncFieldValueSchema = Schema.Union([
|
|
|
82
82
|
export const BaselineCustomFieldSchema = Schema.Struct({
|
|
83
83
|
fieldId: NonEmptyString,
|
|
84
84
|
displayName: NonEmptyString,
|
|
85
|
-
|
|
85
|
+
form: FieldContractSchema,
|
|
86
86
|
value: SyncFieldValueSchema
|
|
87
|
-
})
|
|
87
|
+
}).pipe(Schema.encodeKeys({ form: "shape" }))
|
|
88
88
|
|
|
89
89
|
export const SyncBaselineSchema = Schema.Struct({
|
|
90
90
|
version: Schema.Literal(1),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export type
|
|
7
|
+
export type FieldContract =
|
|
8
8
|
| "text"
|
|
9
9
|
| "multilineText"
|
|
10
10
|
| "number"
|
|
@@ -15,7 +15,7 @@ export type FieldShape =
|
|
|
15
15
|
| "user"
|
|
16
16
|
| "cascadingSelect"
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const FIELD_CONTRACTS: ReadonlyArray<FieldContract> = [
|
|
19
19
|
"text",
|
|
20
20
|
"multilineText",
|
|
21
21
|
"number",
|
|
@@ -30,7 +30,7 @@ export const FIELD_SHAPES: ReadonlyArray<FieldShape> = [
|
|
|
30
30
|
export interface RequestedCustomField {
|
|
31
31
|
readonly displayName: string
|
|
32
32
|
readonly fieldId?: string | undefined
|
|
33
|
-
readonly
|
|
33
|
+
readonly form: FieldContract
|
|
34
34
|
readonly ordered?: boolean | undefined
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -83,7 +83,7 @@ export type SyncFieldValue =
|
|
|
83
83
|
export interface BaselineCustomField {
|
|
84
84
|
readonly fieldId: string
|
|
85
85
|
readonly displayName: string
|
|
86
|
-
readonly
|
|
86
|
+
readonly form: FieldContract
|
|
87
87
|
readonly value: SyncFieldValue
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
2
|
import { JiraApiClient, make } from "@knpkv/jira-api-client"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as HttpClient from "effect/http/HttpClient"
|
|
5
|
+
import * as HttpClientResponse from "effect/http/HttpClientResponse"
|
|
4
6
|
import * as Layer from "effect/Layer"
|
|
5
7
|
import * as Redacted from "effect/Redacted"
|
|
6
|
-
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
7
|
-
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
8
8
|
import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../src/IssueService.js"
|
|
9
9
|
|
|
10
10
|
const httpClient = HttpClient.make((request) =>
|