@shipfox/api-integration-jira 4.0.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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +20 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/tokens.d.ts +36 -0
- package/dist/core/tokens.d.ts.map +1 -0
- package/dist/core/tokens.js +40 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/db/db.d.ts +475 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/installations.d.ts +41 -0
- package/dist/db/installations.d.ts.map +1 -0
- package/dist/db/installations.js +65 -0
- package/dist/db/installations.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/installations.d.ts +239 -0
- package/dist/db/schema/installations.d.ts.map +1 -0
- package/dist/db/schema/installations.js +48 -0
- package/dist/db/schema/installations.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_whole_master_chief.sql +18 -0
- package/drizzle/meta/0000_snapshot.json +145 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +63 -0
- package/src/config.test.ts +37 -0
- package/src/config.ts +27 -0
- package/src/core/errors.ts +22 -0
- package/src/core/tokens.test.ts +106 -0
- package/src/core/tokens.ts +77 -0
- package/src/db/db.ts +16 -0
- package/src/db/installations.test.ts +93 -0
- package/src/db/installations.ts +125 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/installations.ts +48 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +36 -0
- package/test/api-secrets.d.ts +26 -0
- package/test/env.ts +15 -0
- package/test/factories/index.ts +1 -0
- package/test/factories/jira-installation.ts +35 -0
- package/test/globalSetup.ts +23 -0
- package/test/index.ts +1 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {eq, sql} from 'drizzle-orm';
|
|
2
|
+
import {JiraInstallationSiteMismatchError} from '#core/errors.js';
|
|
3
|
+
import {db} from './db.js';
|
|
4
|
+
import {jiraInstallations, toJiraInstallation} from './schema/installations.js';
|
|
5
|
+
|
|
6
|
+
export type JiraInstallationStatus = 'installed' | 'revoked';
|
|
7
|
+
|
|
8
|
+
export interface JiraInstallation {
|
|
9
|
+
id: string;
|
|
10
|
+
connectionId: string;
|
|
11
|
+
cloudId: string;
|
|
12
|
+
siteUrl: string;
|
|
13
|
+
siteName: string;
|
|
14
|
+
authorizingAccountId: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
webhookIds: number[];
|
|
17
|
+
webhookExpiresAt: Date | null;
|
|
18
|
+
status: JiraInstallationStatus;
|
|
19
|
+
tokenExpiresAt: Date | null;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UpsertJiraInstallationParams {
|
|
25
|
+
connectionId: string;
|
|
26
|
+
cloudId: string;
|
|
27
|
+
siteUrl: string;
|
|
28
|
+
siteName: string;
|
|
29
|
+
authorizingAccountId: string;
|
|
30
|
+
scopes: string[];
|
|
31
|
+
webhookIds?: number[] | undefined;
|
|
32
|
+
webhookExpiresAt?: Date | null | undefined;
|
|
33
|
+
status: JiraInstallationStatus;
|
|
34
|
+
tokenExpiresAt?: Date | null | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type JiraDb = ReturnType<typeof db>;
|
|
38
|
+
type JiraTx = Parameters<Parameters<JiraDb['transaction']>[0]>[0];
|
|
39
|
+
|
|
40
|
+
export async function upsertJiraInstallation(
|
|
41
|
+
params: UpsertJiraInstallationParams,
|
|
42
|
+
options: {tx?: unknown} = {},
|
|
43
|
+
): Promise<JiraInstallation> {
|
|
44
|
+
const executor = (options.tx ?? db()) as JiraDb | JiraTx;
|
|
45
|
+
const now = new Date();
|
|
46
|
+
const webhookIds = params.webhookIds ?? [];
|
|
47
|
+
const [row] = await executor
|
|
48
|
+
.insert(jiraInstallations)
|
|
49
|
+
.values({
|
|
50
|
+
connectionId: params.connectionId,
|
|
51
|
+
cloudId: params.cloudId,
|
|
52
|
+
siteUrl: params.siteUrl,
|
|
53
|
+
siteName: params.siteName,
|
|
54
|
+
authorizingAccountId: params.authorizingAccountId,
|
|
55
|
+
scopes: params.scopes,
|
|
56
|
+
webhookIds,
|
|
57
|
+
webhookExpiresAt: params.webhookExpiresAt ?? null,
|
|
58
|
+
status: params.status,
|
|
59
|
+
tokenExpiresAt: params.tokenExpiresAt ?? null,
|
|
60
|
+
})
|
|
61
|
+
.onConflictDoUpdate({
|
|
62
|
+
target: jiraInstallations.connectionId,
|
|
63
|
+
setWhere: eq(jiraInstallations.cloudId, params.cloudId),
|
|
64
|
+
set: {
|
|
65
|
+
cloudId: params.cloudId,
|
|
66
|
+
siteUrl: params.siteUrl,
|
|
67
|
+
siteName: params.siteName,
|
|
68
|
+
authorizingAccountId: params.authorizingAccountId,
|
|
69
|
+
scopes: params.scopes,
|
|
70
|
+
webhookIds,
|
|
71
|
+
webhookExpiresAt: params.webhookExpiresAt ?? null,
|
|
72
|
+
status: params.status,
|
|
73
|
+
tokenExpiresAt: params.tokenExpiresAt ?? null,
|
|
74
|
+
updatedAt: now,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
.returning();
|
|
78
|
+
|
|
79
|
+
if (!row) throw new JiraInstallationSiteMismatchError(params.connectionId, params.cloudId);
|
|
80
|
+
return toJiraInstallation(row);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function getJiraInstallationByConnectionId(
|
|
84
|
+
connectionId: string,
|
|
85
|
+
options: {tx?: unknown} = {},
|
|
86
|
+
): Promise<JiraInstallation | undefined> {
|
|
87
|
+
const executor = (options.tx ?? db()) as JiraDb | JiraTx;
|
|
88
|
+
const rows = await executor
|
|
89
|
+
.select()
|
|
90
|
+
.from(jiraInstallations)
|
|
91
|
+
.where(eq(jiraInstallations.connectionId, connectionId))
|
|
92
|
+
.limit(1);
|
|
93
|
+
const row = rows[0];
|
|
94
|
+
if (!row) return undefined;
|
|
95
|
+
return toJiraInstallation(row);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getJiraInstallationByWebhookId(
|
|
99
|
+
webhookId: number,
|
|
100
|
+
options: {tx?: unknown} = {},
|
|
101
|
+
): Promise<JiraInstallation | undefined> {
|
|
102
|
+
const executor = (options.tx ?? db()) as JiraDb | JiraTx;
|
|
103
|
+
const rows = await executor
|
|
104
|
+
.select()
|
|
105
|
+
.from(jiraInstallations)
|
|
106
|
+
.where(sql`${jiraInstallations.webhookIds} @> ${JSON.stringify([webhookId])}::jsonb`)
|
|
107
|
+
.limit(1);
|
|
108
|
+
const row = rows[0];
|
|
109
|
+
if (!row) return undefined;
|
|
110
|
+
return toJiraInstallation(row);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function markJiraInstallationRevoked(
|
|
114
|
+
connectionId: string,
|
|
115
|
+
options: {tx?: unknown} = {},
|
|
116
|
+
): Promise<JiraInstallation | undefined> {
|
|
117
|
+
const executor = (options.tx ?? db()) as JiraDb | JiraTx;
|
|
118
|
+
const [row] = await executor
|
|
119
|
+
.update(jiraInstallations)
|
|
120
|
+
.set({status: 'revoked', updatedAt: new Date()})
|
|
121
|
+
.where(eq(jiraInstallations.connectionId, connectionId))
|
|
122
|
+
.returning();
|
|
123
|
+
if (!row) return undefined;
|
|
124
|
+
return toJiraInstallation(row);
|
|
125
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {index, jsonb, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
3
|
+
import type {JiraInstallation} from '#db/installations.js';
|
|
4
|
+
import {pgTable} from './common.js';
|
|
5
|
+
|
|
6
|
+
export const jiraInstallations = pgTable(
|
|
7
|
+
'installations',
|
|
8
|
+
{
|
|
9
|
+
id: uuidv7PrimaryKey(),
|
|
10
|
+
connectionId: uuid('connection_id').notNull(),
|
|
11
|
+
cloudId: text('cloud_id').notNull(),
|
|
12
|
+
siteUrl: text('site_url').notNull(),
|
|
13
|
+
siteName: text('site_name').notNull(),
|
|
14
|
+
authorizingAccountId: text('authorizing_account_id').notNull(),
|
|
15
|
+
scopes: jsonb('scopes').$type<string[]>().notNull(),
|
|
16
|
+
webhookIds: jsonb('webhook_ids').$type<number[]>().notNull().default([]),
|
|
17
|
+
webhookExpiresAt: timestamp('webhook_expires_at', {withTimezone: true}),
|
|
18
|
+
status: text('status').notNull().$type<JiraInstallation['status']>(),
|
|
19
|
+
tokenExpiresAt: timestamp('token_expires_at', {withTimezone: true}),
|
|
20
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
21
|
+
updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
|
|
22
|
+
},
|
|
23
|
+
(table) => [
|
|
24
|
+
uniqueIndex('integrations_jira_installations_connection_unique').on(table.connectionId),
|
|
25
|
+
index('integrations_jira_installations_cloud_id_idx').on(table.cloudId),
|
|
26
|
+
],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export type JiraInstallationDb = typeof jiraInstallations.$inferSelect;
|
|
30
|
+
export type JiraInstallationCreateDb = typeof jiraInstallations.$inferInsert;
|
|
31
|
+
|
|
32
|
+
export function toJiraInstallation(row: JiraInstallationDb): JiraInstallation {
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
connectionId: row.connectionId,
|
|
36
|
+
cloudId: row.cloudId,
|
|
37
|
+
siteUrl: row.siteUrl,
|
|
38
|
+
siteName: row.siteName,
|
|
39
|
+
authorizingAccountId: row.authorizingAccountId,
|
|
40
|
+
scopes: row.scopes,
|
|
41
|
+
webhookIds: row.webhookIds,
|
|
42
|
+
webhookExpiresAt: row.webhookExpiresAt,
|
|
43
|
+
status: row.status,
|
|
44
|
+
tokenExpiresAt: row.tokenExpiresAt,
|
|
45
|
+
createdAt: row.createdAt,
|
|
46
|
+
updatedAt: row.updatedAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {createJiraIntegrationProvider} from '#index.js';
|
|
2
|
+
|
|
3
|
+
describe('createJiraIntegrationProvider', () => {
|
|
4
|
+
it('creates the minimal Jira provider', () => {
|
|
5
|
+
const provider = createJiraIntegrationProvider();
|
|
6
|
+
|
|
7
|
+
expect(provider).toEqual({provider: 'jira', displayName: 'Jira'});
|
|
8
|
+
});
|
|
9
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {JIRA_PROVIDER} from '@shipfox/api-integration-jira-dto';
|
|
2
|
+
import {config} from '#config.js';
|
|
3
|
+
import {closeDb, db} from '#db/db.js';
|
|
4
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
5
|
+
|
|
6
|
+
export type {JiraProvider} from '@shipfox/api-integration-jira-dto';
|
|
7
|
+
export {
|
|
8
|
+
JiraAccessTokenMissingError,
|
|
9
|
+
JiraConnectionNotFoundError,
|
|
10
|
+
JiraInstallationSiteMismatchError,
|
|
11
|
+
} from '#core/errors.js';
|
|
12
|
+
export type {
|
|
13
|
+
CreateJiraTokenStoreParams,
|
|
14
|
+
GetJiraAccessTokenParams,
|
|
15
|
+
JiraConnectionResolverResult,
|
|
16
|
+
JiraSecretsStore,
|
|
17
|
+
JiraTokenStore,
|
|
18
|
+
StoreJiraTokensParams,
|
|
19
|
+
} from '#core/tokens.js';
|
|
20
|
+
export {createJiraTokenStore, jiraSecretsNamespace} from '#core/tokens.js';
|
|
21
|
+
export type {
|
|
22
|
+
JiraInstallation,
|
|
23
|
+
JiraInstallationStatus,
|
|
24
|
+
UpsertJiraInstallationParams,
|
|
25
|
+
} from '#db/installations.js';
|
|
26
|
+
export {
|
|
27
|
+
getJiraInstallationByConnectionId,
|
|
28
|
+
getJiraInstallationByWebhookId,
|
|
29
|
+
markJiraInstallationRevoked,
|
|
30
|
+
upsertJiraInstallation,
|
|
31
|
+
} from '#db/installations.js';
|
|
32
|
+
export {closeDb, config, db, migrationsPath};
|
|
33
|
+
|
|
34
|
+
export function createJiraIntegrationProvider() {
|
|
35
|
+
return {provider: JIRA_PROVIDER, displayName: 'Jira'};
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare module '@shipfox/api-secrets' {
|
|
2
|
+
import type {NodePgDatabase} from '@shipfox/node-drizzle';
|
|
3
|
+
|
|
4
|
+
export function getSecret(params: {
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
key: string;
|
|
8
|
+
}): Promise<string | null>;
|
|
9
|
+
|
|
10
|
+
export function setSecrets(params: {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
namespace: string;
|
|
13
|
+
values: Record<string, string>;
|
|
14
|
+
editedBy?: string | null | undefined;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
|
|
17
|
+
export const secretsModule: {
|
|
18
|
+
database?:
|
|
19
|
+
| {
|
|
20
|
+
db(): NodePgDatabase<Record<string, unknown>>;
|
|
21
|
+
migrationsPath: string;
|
|
22
|
+
}
|
|
23
|
+
| unknown[]
|
|
24
|
+
| undefined;
|
|
25
|
+
};
|
|
26
|
+
}
|
package/test/env.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
process.env.POSTGRES_HOST ??= 'localhost';
|
|
2
|
+
process.env.POSTGRES_PORT ??= '5432';
|
|
3
|
+
process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
4
|
+
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
|
+
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
|
+
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
+
process.env.TZ = 'UTC';
|
|
8
|
+
process.env.JIRA_OAUTH_CLIENT_ID = 'test-client-id';
|
|
9
|
+
process.env.JIRA_OAUTH_CLIENT_SECRET = 'test-client-secret';
|
|
10
|
+
process.env.JIRA_OAUTH_REDIRECT_URL = 'https://shipfox.example.com/integrations/jira/callback';
|
|
11
|
+
process.env.JIRA_WEBHOOK_SIGNING_SECRET = 'test-signing-secret';
|
|
12
|
+
process.env.JIRA_WEBHOOK_BASE_URL = 'https://shipfox.example.com';
|
|
13
|
+
process.env.JIRA_API_BASE_URL = 'http://127.0.0.1:0';
|
|
14
|
+
process.env.JIRA_AUTH_BASE_URL = 'http://127.0.0.1:0';
|
|
15
|
+
process.env.SECRETS_ENCRYPTION_KEK = 'ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {jiraInstallationFactory} from './jira-installation.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {Factory} from 'fishery';
|
|
2
|
+
import {type JiraInstallation, upsertJiraInstallation} from '#db/installations.js';
|
|
3
|
+
|
|
4
|
+
export const jiraInstallationFactory = Factory.define<JiraInstallation>(({sequence, onCreate}) => {
|
|
5
|
+
onCreate((installation) =>
|
|
6
|
+
upsertJiraInstallation({
|
|
7
|
+
connectionId: installation.connectionId,
|
|
8
|
+
cloudId: installation.cloudId,
|
|
9
|
+
siteUrl: installation.siteUrl,
|
|
10
|
+
siteName: installation.siteName,
|
|
11
|
+
authorizingAccountId: installation.authorizingAccountId,
|
|
12
|
+
scopes: installation.scopes,
|
|
13
|
+
webhookIds: installation.webhookIds,
|
|
14
|
+
webhookExpiresAt: installation.webhookExpiresAt,
|
|
15
|
+
status: installation.status,
|
|
16
|
+
tokenExpiresAt: installation.tokenExpiresAt,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: crypto.randomUUID(),
|
|
22
|
+
connectionId: crypto.randomUUID(),
|
|
23
|
+
cloudId: crypto.randomUUID(),
|
|
24
|
+
siteUrl: 'https://acme.atlassian.net',
|
|
25
|
+
siteName: 'Acme',
|
|
26
|
+
authorizingAccountId: crypto.randomUUID(),
|
|
27
|
+
scopes: ['read:jira-work'],
|
|
28
|
+
webhookIds: [sequence + 1],
|
|
29
|
+
webhookExpiresAt: null,
|
|
30
|
+
status: 'installed',
|
|
31
|
+
tokenExpiresAt: null,
|
|
32
|
+
createdAt: new Date(),
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {runMigrations} from '@shipfox/node-drizzle';
|
|
3
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
4
|
+
import {closeDb, db} from '#db/db.js';
|
|
5
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
6
|
+
|
|
7
|
+
export async function setup() {
|
|
8
|
+
createPostgresClient();
|
|
9
|
+
|
|
10
|
+
await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations_jira');
|
|
11
|
+
const {secretsModule} = await import('@shipfox/api-secrets');
|
|
12
|
+
if (!secretsModule.database || Array.isArray(secretsModule.database)) {
|
|
13
|
+
throw new Error('Secrets module database is not configured');
|
|
14
|
+
}
|
|
15
|
+
await runMigrations(
|
|
16
|
+
secretsModule.database.db(),
|
|
17
|
+
secretsModule.database.migrationsPath,
|
|
18
|
+
'__drizzle_migrations_secrets',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
closeDb();
|
|
22
|
+
await closePostgresClient();
|
|
23
|
+
}
|
package/test/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {jiraInstallationFactory} from './factories/jira-installation.js';
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
|
|
4
|
+
import {closeDb} from '#db/db.js';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
createPostgresClient();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
closeDb();
|
|
16
|
+
await closePostgresClient();
|
|
17
|
+
});
|