@meridianjs/issue 0.1.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/dist/index.d.mts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +404 -0
- package/dist/index.mjs +378 -0
- package/package.json +43 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as _meridianjs_types from '@meridianjs/types';
|
|
2
|
+
import { MeridianContainer } from '@meridianjs/types';
|
|
3
|
+
|
|
4
|
+
interface CreateIssueInput {
|
|
5
|
+
title: string;
|
|
6
|
+
project_id: string;
|
|
7
|
+
workspace_id: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
priority?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
assignee_ids?: string[];
|
|
13
|
+
reporter_id?: string;
|
|
14
|
+
parent_id?: string | null;
|
|
15
|
+
due_date?: Date;
|
|
16
|
+
estimate?: number;
|
|
17
|
+
sprint_id?: string | null;
|
|
18
|
+
task_list_id?: string | null;
|
|
19
|
+
}
|
|
20
|
+
interface CreateAttachmentInput {
|
|
21
|
+
issue_id: string;
|
|
22
|
+
comment_id?: string | null;
|
|
23
|
+
filename: string;
|
|
24
|
+
original_name: string;
|
|
25
|
+
mime_type: string;
|
|
26
|
+
size: number;
|
|
27
|
+
url: string;
|
|
28
|
+
uploader_id: string;
|
|
29
|
+
workspace_id: string;
|
|
30
|
+
}
|
|
31
|
+
interface CreateManualTimeLogInput {
|
|
32
|
+
issue_id: string;
|
|
33
|
+
user_id: string;
|
|
34
|
+
workspace_id: string;
|
|
35
|
+
duration_minutes: number;
|
|
36
|
+
description?: string;
|
|
37
|
+
logged_date?: Date;
|
|
38
|
+
}
|
|
39
|
+
declare const IssueModuleService_base: new (container: MeridianContainer) => _meridianjs_types.IModuleService;
|
|
40
|
+
declare class IssueModuleService extends IssueModuleService_base {
|
|
41
|
+
private readonly container;
|
|
42
|
+
constructor(container: MeridianContainer);
|
|
43
|
+
/**
|
|
44
|
+
* Create an issue with an auto-generated sequential identifier (e.g. PROJ-42).
|
|
45
|
+
* Looks up the project to get its identifier prefix, then assigns the next number.
|
|
46
|
+
*/
|
|
47
|
+
createIssueInProject(input: CreateIssueInput): Promise<any>;
|
|
48
|
+
/** List issues for a project with optional filters. */
|
|
49
|
+
listIssuesByProject(projectId: string, filters?: {
|
|
50
|
+
status?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
}, options?: {
|
|
53
|
+
limit?: number;
|
|
54
|
+
offset?: number;
|
|
55
|
+
}): Promise<any[]>;
|
|
56
|
+
/** List all comments for an issue. */
|
|
57
|
+
listCommentsByIssue(issueId: string): Promise<any[]>;
|
|
58
|
+
/** Add a comment to an issue. */
|
|
59
|
+
createComment(input: {
|
|
60
|
+
issue_id: string;
|
|
61
|
+
body: string;
|
|
62
|
+
author_id: string;
|
|
63
|
+
}): Promise<any>;
|
|
64
|
+
/** List all attachments for an issue, ordered by creation date. */
|
|
65
|
+
listAttachmentsByIssue(issueId: string): Promise<any[]>;
|
|
66
|
+
/** Persist an attachment record after the file has been stored on disk. */
|
|
67
|
+
createAttachment(input: CreateAttachmentInput): Promise<any>;
|
|
68
|
+
/** Delete an attachment record by ID. The caller is responsible for removing the file. */
|
|
69
|
+
deleteAttachment(attachmentId: string): Promise<any>;
|
|
70
|
+
/** List all time log entries for an issue, newest first. */
|
|
71
|
+
listTimeLogsByIssue(issueId: string): Promise<any[]>;
|
|
72
|
+
/** Create a manual time log entry with an explicit duration. */
|
|
73
|
+
createManualTimeLog(input: CreateManualTimeLogInput): Promise<any>;
|
|
74
|
+
/**
|
|
75
|
+
* Start a timer for a user on an issue.
|
|
76
|
+
* Throws if the user already has an active timer on this issue.
|
|
77
|
+
*/
|
|
78
|
+
startTimer(issueId: string, userId: string, workspaceId: string): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Stop the active timer for a user on an issue.
|
|
81
|
+
* Calculates duration from started_at and finalises the entry.
|
|
82
|
+
*/
|
|
83
|
+
stopTimer(issueId: string, userId: string): Promise<any>;
|
|
84
|
+
/** Return the running timer entry for a user on an issue, or null if none. */
|
|
85
|
+
getActiveTimer(issueId: string, userId: string): Promise<any | null>;
|
|
86
|
+
/** Delete a time log entry by ID. */
|
|
87
|
+
deleteTimeLog(id: string): Promise<any>;
|
|
88
|
+
/** List all task lists for a project, ordered by position. */
|
|
89
|
+
listTaskListsByProject(projectId: string): Promise<any[]>;
|
|
90
|
+
/** Create a task list for a project. Position defaults to max+1. */
|
|
91
|
+
createTaskList(input: {
|
|
92
|
+
name: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
project_id: string;
|
|
95
|
+
}): Promise<any>;
|
|
96
|
+
/** Update a task list's name or description. */
|
|
97
|
+
updateTaskList(id: string, data: {
|
|
98
|
+
name?: string;
|
|
99
|
+
description?: string;
|
|
100
|
+
}): Promise<any>;
|
|
101
|
+
/** Delete a task list by ID. Clears task_list_id on associated issues. */
|
|
102
|
+
deleteTaskList(id: string): Promise<any>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare const ISSUE_MODULE = "issueModuleService";
|
|
106
|
+
declare const _default: _meridianjs_types.ModuleDefinition;
|
|
107
|
+
|
|
108
|
+
export { ISSUE_MODULE, IssueModuleService, _default as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as _meridianjs_types from '@meridianjs/types';
|
|
2
|
+
import { MeridianContainer } from '@meridianjs/types';
|
|
3
|
+
|
|
4
|
+
interface CreateIssueInput {
|
|
5
|
+
title: string;
|
|
6
|
+
project_id: string;
|
|
7
|
+
workspace_id: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
priority?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
assignee_ids?: string[];
|
|
13
|
+
reporter_id?: string;
|
|
14
|
+
parent_id?: string | null;
|
|
15
|
+
due_date?: Date;
|
|
16
|
+
estimate?: number;
|
|
17
|
+
sprint_id?: string | null;
|
|
18
|
+
task_list_id?: string | null;
|
|
19
|
+
}
|
|
20
|
+
interface CreateAttachmentInput {
|
|
21
|
+
issue_id: string;
|
|
22
|
+
comment_id?: string | null;
|
|
23
|
+
filename: string;
|
|
24
|
+
original_name: string;
|
|
25
|
+
mime_type: string;
|
|
26
|
+
size: number;
|
|
27
|
+
url: string;
|
|
28
|
+
uploader_id: string;
|
|
29
|
+
workspace_id: string;
|
|
30
|
+
}
|
|
31
|
+
interface CreateManualTimeLogInput {
|
|
32
|
+
issue_id: string;
|
|
33
|
+
user_id: string;
|
|
34
|
+
workspace_id: string;
|
|
35
|
+
duration_minutes: number;
|
|
36
|
+
description?: string;
|
|
37
|
+
logged_date?: Date;
|
|
38
|
+
}
|
|
39
|
+
declare const IssueModuleService_base: new (container: MeridianContainer) => _meridianjs_types.IModuleService;
|
|
40
|
+
declare class IssueModuleService extends IssueModuleService_base {
|
|
41
|
+
private readonly container;
|
|
42
|
+
constructor(container: MeridianContainer);
|
|
43
|
+
/**
|
|
44
|
+
* Create an issue with an auto-generated sequential identifier (e.g. PROJ-42).
|
|
45
|
+
* Looks up the project to get its identifier prefix, then assigns the next number.
|
|
46
|
+
*/
|
|
47
|
+
createIssueInProject(input: CreateIssueInput): Promise<any>;
|
|
48
|
+
/** List issues for a project with optional filters. */
|
|
49
|
+
listIssuesByProject(projectId: string, filters?: {
|
|
50
|
+
status?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
}, options?: {
|
|
53
|
+
limit?: number;
|
|
54
|
+
offset?: number;
|
|
55
|
+
}): Promise<any[]>;
|
|
56
|
+
/** List all comments for an issue. */
|
|
57
|
+
listCommentsByIssue(issueId: string): Promise<any[]>;
|
|
58
|
+
/** Add a comment to an issue. */
|
|
59
|
+
createComment(input: {
|
|
60
|
+
issue_id: string;
|
|
61
|
+
body: string;
|
|
62
|
+
author_id: string;
|
|
63
|
+
}): Promise<any>;
|
|
64
|
+
/** List all attachments for an issue, ordered by creation date. */
|
|
65
|
+
listAttachmentsByIssue(issueId: string): Promise<any[]>;
|
|
66
|
+
/** Persist an attachment record after the file has been stored on disk. */
|
|
67
|
+
createAttachment(input: CreateAttachmentInput): Promise<any>;
|
|
68
|
+
/** Delete an attachment record by ID. The caller is responsible for removing the file. */
|
|
69
|
+
deleteAttachment(attachmentId: string): Promise<any>;
|
|
70
|
+
/** List all time log entries for an issue, newest first. */
|
|
71
|
+
listTimeLogsByIssue(issueId: string): Promise<any[]>;
|
|
72
|
+
/** Create a manual time log entry with an explicit duration. */
|
|
73
|
+
createManualTimeLog(input: CreateManualTimeLogInput): Promise<any>;
|
|
74
|
+
/**
|
|
75
|
+
* Start a timer for a user on an issue.
|
|
76
|
+
* Throws if the user already has an active timer on this issue.
|
|
77
|
+
*/
|
|
78
|
+
startTimer(issueId: string, userId: string, workspaceId: string): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Stop the active timer for a user on an issue.
|
|
81
|
+
* Calculates duration from started_at and finalises the entry.
|
|
82
|
+
*/
|
|
83
|
+
stopTimer(issueId: string, userId: string): Promise<any>;
|
|
84
|
+
/** Return the running timer entry for a user on an issue, or null if none. */
|
|
85
|
+
getActiveTimer(issueId: string, userId: string): Promise<any | null>;
|
|
86
|
+
/** Delete a time log entry by ID. */
|
|
87
|
+
deleteTimeLog(id: string): Promise<any>;
|
|
88
|
+
/** List all task lists for a project, ordered by position. */
|
|
89
|
+
listTaskListsByProject(projectId: string): Promise<any[]>;
|
|
90
|
+
/** Create a task list for a project. Position defaults to max+1. */
|
|
91
|
+
createTaskList(input: {
|
|
92
|
+
name: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
project_id: string;
|
|
95
|
+
}): Promise<any>;
|
|
96
|
+
/** Update a task list's name or description. */
|
|
97
|
+
updateTaskList(id: string, data: {
|
|
98
|
+
name?: string;
|
|
99
|
+
description?: string;
|
|
100
|
+
}): Promise<any>;
|
|
101
|
+
/** Delete a task list by ID. Clears task_list_id on associated issues. */
|
|
102
|
+
deleteTaskList(id: string): Promise<any>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare const ISSUE_MODULE = "issueModuleService";
|
|
106
|
+
declare const _default: _meridianjs_types.ModuleDefinition;
|
|
107
|
+
|
|
108
|
+
export { ISSUE_MODULE, IssueModuleService, _default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ISSUE_MODULE: () => ISSUE_MODULE,
|
|
24
|
+
IssueModuleService: () => IssueModuleService,
|
|
25
|
+
default: () => index_default
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_framework_utils8 = require("@meridianjs/framework-utils");
|
|
29
|
+
|
|
30
|
+
// src/service.ts
|
|
31
|
+
var import_framework_utils6 = require("@meridianjs/framework-utils");
|
|
32
|
+
|
|
33
|
+
// src/models/issue.ts
|
|
34
|
+
var import_framework_utils = require("@meridianjs/framework-utils");
|
|
35
|
+
var Issue = import_framework_utils.model.define("issue", {
|
|
36
|
+
id: import_framework_utils.model.id().primaryKey(),
|
|
37
|
+
/** Full identifier, e.g. "PROJ-42" */
|
|
38
|
+
identifier: import_framework_utils.model.text(),
|
|
39
|
+
/** Sequential number within the project, e.g. 42 */
|
|
40
|
+
number: import_framework_utils.model.number(),
|
|
41
|
+
title: import_framework_utils.model.text(),
|
|
42
|
+
description: import_framework_utils.model.text().nullable(),
|
|
43
|
+
type: import_framework_utils.model.enum(["bug", "feature", "task", "epic", "story"]).default("task"),
|
|
44
|
+
priority: import_framework_utils.model.enum(["urgent", "high", "medium", "low", "none"]).default("none"),
|
|
45
|
+
status: import_framework_utils.model.text().default("backlog"),
|
|
46
|
+
/** Denormalized — not a FK */
|
|
47
|
+
project_id: import_framework_utils.model.text(),
|
|
48
|
+
workspace_id: import_framework_utils.model.text(),
|
|
49
|
+
/** Array of user IDs assigned to this issue */
|
|
50
|
+
assignee_ids: import_framework_utils.model.json().nullable(),
|
|
51
|
+
reporter_id: import_framework_utils.model.text().nullable(),
|
|
52
|
+
/** Parent issue ID for subtasks */
|
|
53
|
+
parent_id: import_framework_utils.model.text().nullable(),
|
|
54
|
+
/** Denormalized sprint reference — no FK constraint */
|
|
55
|
+
sprint_id: import_framework_utils.model.text().nullable(),
|
|
56
|
+
/** Denormalized task list reference — no FK constraint */
|
|
57
|
+
task_list_id: import_framework_utils.model.text().nullable(),
|
|
58
|
+
due_date: import_framework_utils.model.date().nullable(),
|
|
59
|
+
/** Story point estimate */
|
|
60
|
+
estimate: import_framework_utils.model.number().nullable()
|
|
61
|
+
}, [
|
|
62
|
+
{ columns: ["project_id"] },
|
|
63
|
+
{ columns: ["workspace_id"] },
|
|
64
|
+
{ columns: ["project_id", "status"] }
|
|
65
|
+
]);
|
|
66
|
+
var issue_default = Issue;
|
|
67
|
+
|
|
68
|
+
// src/models/comment.ts
|
|
69
|
+
var import_framework_utils2 = require("@meridianjs/framework-utils");
|
|
70
|
+
var Comment = import_framework_utils2.model.define("comment", {
|
|
71
|
+
id: import_framework_utils2.model.id().primaryKey(),
|
|
72
|
+
body: import_framework_utils2.model.text(),
|
|
73
|
+
issue_id: import_framework_utils2.model.text(),
|
|
74
|
+
author_id: import_framework_utils2.model.text(),
|
|
75
|
+
edited_at: import_framework_utils2.model.date().nullable()
|
|
76
|
+
});
|
|
77
|
+
var comment_default = Comment;
|
|
78
|
+
|
|
79
|
+
// src/models/attachment.ts
|
|
80
|
+
var import_framework_utils3 = require("@meridianjs/framework-utils");
|
|
81
|
+
var Attachment = import_framework_utils3.model.define("attachment", {
|
|
82
|
+
id: import_framework_utils3.model.id().primaryKey(),
|
|
83
|
+
issue_id: import_framework_utils3.model.text(),
|
|
84
|
+
/** Set when the attachment belongs to a specific comment; null for issue-level attachments */
|
|
85
|
+
comment_id: import_framework_utils3.model.text().nullable(),
|
|
86
|
+
/** UUID-based stored filename on disk */
|
|
87
|
+
filename: import_framework_utils3.model.text(),
|
|
88
|
+
/** Original filename from the upload */
|
|
89
|
+
original_name: import_framework_utils3.model.text(),
|
|
90
|
+
mime_type: import_framework_utils3.model.text(),
|
|
91
|
+
/** File size in bytes */
|
|
92
|
+
size: import_framework_utils3.model.number(),
|
|
93
|
+
/** Publicly accessible URL, e.g. /uploads/issue-attachments/<uuid>-<name> */
|
|
94
|
+
url: import_framework_utils3.model.text(),
|
|
95
|
+
uploader_id: import_framework_utils3.model.text(),
|
|
96
|
+
workspace_id: import_framework_utils3.model.text()
|
|
97
|
+
}, [
|
|
98
|
+
{ columns: ["issue_id"] },
|
|
99
|
+
{ columns: ["comment_id"] }
|
|
100
|
+
]);
|
|
101
|
+
var attachment_default = Attachment;
|
|
102
|
+
|
|
103
|
+
// src/models/time-log.ts
|
|
104
|
+
var import_framework_utils4 = require("@meridianjs/framework-utils");
|
|
105
|
+
var TimeLog = import_framework_utils4.model.define("time_log", {
|
|
106
|
+
id: import_framework_utils4.model.id().primaryKey(),
|
|
107
|
+
issue_id: import_framework_utils4.model.text(),
|
|
108
|
+
user_id: import_framework_utils4.model.text(),
|
|
109
|
+
workspace_id: import_framework_utils4.model.text(),
|
|
110
|
+
/** Total duration in minutes. null when a timer is still running. */
|
|
111
|
+
duration_minutes: import_framework_utils4.model.number().nullable(),
|
|
112
|
+
description: import_framework_utils4.model.text().nullable(),
|
|
113
|
+
/** The calendar date for this time entry (manual entries) */
|
|
114
|
+
logged_date: import_framework_utils4.model.date().nullable(),
|
|
115
|
+
/** Set when the timer starts; null for manual entries */
|
|
116
|
+
started_at: import_framework_utils4.model.date().nullable(),
|
|
117
|
+
/** Set when the timer stops; null while timer is running */
|
|
118
|
+
stopped_at: import_framework_utils4.model.date().nullable(),
|
|
119
|
+
source: import_framework_utils4.model.enum(["manual", "timer"]).default("manual")
|
|
120
|
+
}, [
|
|
121
|
+
{ columns: ["issue_id"] },
|
|
122
|
+
{ columns: ["user_id"] }
|
|
123
|
+
]);
|
|
124
|
+
var time_log_default = TimeLog;
|
|
125
|
+
|
|
126
|
+
// src/models/task-list.ts
|
|
127
|
+
var import_framework_utils5 = require("@meridianjs/framework-utils");
|
|
128
|
+
var TaskList = import_framework_utils5.model.define("task_list", {
|
|
129
|
+
id: import_framework_utils5.model.id().primaryKey(),
|
|
130
|
+
name: import_framework_utils5.model.text(),
|
|
131
|
+
description: import_framework_utils5.model.text().nullable(),
|
|
132
|
+
/** Denormalized — not a FK */
|
|
133
|
+
project_id: import_framework_utils5.model.text(),
|
|
134
|
+
position: import_framework_utils5.model.number().default(0)
|
|
135
|
+
}, [
|
|
136
|
+
{ columns: ["project_id"] }
|
|
137
|
+
]);
|
|
138
|
+
var task_list_default = TaskList;
|
|
139
|
+
|
|
140
|
+
// src/service.ts
|
|
141
|
+
var IssueModuleService = class extends (0, import_framework_utils6.MeridianService)({
|
|
142
|
+
Issue: issue_default,
|
|
143
|
+
Comment: comment_default,
|
|
144
|
+
Attachment: attachment_default,
|
|
145
|
+
TimeLog: time_log_default,
|
|
146
|
+
TaskList: task_list_default
|
|
147
|
+
}) {
|
|
148
|
+
container;
|
|
149
|
+
constructor(container) {
|
|
150
|
+
super(container);
|
|
151
|
+
this.container = container;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create an issue with an auto-generated sequential identifier (e.g. PROJ-42).
|
|
155
|
+
* Looks up the project to get its identifier prefix, then assigns the next number.
|
|
156
|
+
*/
|
|
157
|
+
async createIssueInProject(input) {
|
|
158
|
+
const issueRepo = this.container.resolve("issueRepository");
|
|
159
|
+
const projectService = this.container.resolve("projectModuleService");
|
|
160
|
+
const project = await projectService.retrieveProject(input.project_id);
|
|
161
|
+
if (!project) {
|
|
162
|
+
throw Object.assign(new Error(`Project ${input.project_id} not found`), { status: 404 });
|
|
163
|
+
}
|
|
164
|
+
const existing = await issueRepo.find({ project_id: input.project_id });
|
|
165
|
+
const maxNumber = existing.reduce(
|
|
166
|
+
(max, issue2) => Math.max(max, issue2.number ?? 0),
|
|
167
|
+
0
|
|
168
|
+
);
|
|
169
|
+
const nextNumber = maxNumber + 1;
|
|
170
|
+
const identifier = `${project.identifier}-${nextNumber}`;
|
|
171
|
+
const issue = issueRepo.create({
|
|
172
|
+
...input,
|
|
173
|
+
number: nextNumber,
|
|
174
|
+
identifier,
|
|
175
|
+
type: input.type ?? "task",
|
|
176
|
+
priority: input.priority ?? "none",
|
|
177
|
+
status: input.status ?? "backlog"
|
|
178
|
+
});
|
|
179
|
+
await issueRepo.persistAndFlush(issue);
|
|
180
|
+
return issue;
|
|
181
|
+
}
|
|
182
|
+
/** List issues for a project with optional filters. */
|
|
183
|
+
async listIssuesByProject(projectId, filters = {}, options = {}) {
|
|
184
|
+
const repo = this.container.resolve("issueRepository");
|
|
185
|
+
return repo.find(
|
|
186
|
+
{ project_id: projectId, ...filters },
|
|
187
|
+
{ limit: options.limit ?? 50, offset: options.offset ?? 0 }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
/** List all comments for an issue. */
|
|
191
|
+
async listCommentsByIssue(issueId) {
|
|
192
|
+
const repo = this.container.resolve("commentRepository");
|
|
193
|
+
return repo.find({ issue_id: issueId });
|
|
194
|
+
}
|
|
195
|
+
/** Add a comment to an issue. */
|
|
196
|
+
async createComment(input) {
|
|
197
|
+
const repo = this.container.resolve("commentRepository");
|
|
198
|
+
const comment = repo.create(input);
|
|
199
|
+
await repo.persistAndFlush(comment);
|
|
200
|
+
return comment;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Attachments
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/** List all attachments for an issue, ordered by creation date. */
|
|
206
|
+
async listAttachmentsByIssue(issueId) {
|
|
207
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
208
|
+
return repo.find({ issue_id: issueId }, { orderBy: { created_at: "ASC" } });
|
|
209
|
+
}
|
|
210
|
+
/** Persist an attachment record after the file has been stored on disk. */
|
|
211
|
+
async createAttachment(input) {
|
|
212
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
213
|
+
const attachment = repo.create(input);
|
|
214
|
+
await repo.persistAndFlush(attachment);
|
|
215
|
+
return attachment;
|
|
216
|
+
}
|
|
217
|
+
/** Delete an attachment record by ID. The caller is responsible for removing the file. */
|
|
218
|
+
async deleteAttachment(attachmentId) {
|
|
219
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
220
|
+
const attachment = await repo.findOne({ id: attachmentId });
|
|
221
|
+
if (!attachment) {
|
|
222
|
+
throw Object.assign(new Error(`Attachment ${attachmentId} not found`), { status: 404 });
|
|
223
|
+
}
|
|
224
|
+
await repo.removeAndFlush(attachment);
|
|
225
|
+
return attachment;
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Time Logging
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
/** List all time log entries for an issue, newest first. */
|
|
231
|
+
async listTimeLogsByIssue(issueId) {
|
|
232
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
233
|
+
return repo.find({ issue_id: issueId }, { orderBy: { created_at: "DESC" } });
|
|
234
|
+
}
|
|
235
|
+
/** Create a manual time log entry with an explicit duration. */
|
|
236
|
+
async createManualTimeLog(input) {
|
|
237
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
238
|
+
const entry = repo.create({
|
|
239
|
+
...input,
|
|
240
|
+
source: "manual",
|
|
241
|
+
logged_date: input.logged_date ?? /* @__PURE__ */ new Date()
|
|
242
|
+
});
|
|
243
|
+
await repo.persistAndFlush(entry);
|
|
244
|
+
return entry;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Start a timer for a user on an issue.
|
|
248
|
+
* Throws if the user already has an active timer on this issue.
|
|
249
|
+
*/
|
|
250
|
+
async startTimer(issueId, userId, workspaceId) {
|
|
251
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
252
|
+
const active = await repo.findOne({
|
|
253
|
+
issue_id: issueId,
|
|
254
|
+
user_id: userId,
|
|
255
|
+
started_at: { $ne: null },
|
|
256
|
+
stopped_at: null
|
|
257
|
+
});
|
|
258
|
+
if (active) {
|
|
259
|
+
throw Object.assign(
|
|
260
|
+
new Error("A timer is already running for this issue. Stop it before starting a new one."),
|
|
261
|
+
{ status: 409 }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const entry = repo.create({
|
|
265
|
+
issue_id: issueId,
|
|
266
|
+
user_id: userId,
|
|
267
|
+
workspace_id: workspaceId,
|
|
268
|
+
source: "timer",
|
|
269
|
+
started_at: /* @__PURE__ */ new Date()
|
|
270
|
+
});
|
|
271
|
+
await repo.persistAndFlush(entry);
|
|
272
|
+
return entry;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Stop the active timer for a user on an issue.
|
|
276
|
+
* Calculates duration from started_at and finalises the entry.
|
|
277
|
+
*/
|
|
278
|
+
async stopTimer(issueId, userId) {
|
|
279
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
280
|
+
const active = await repo.findOne({
|
|
281
|
+
issue_id: issueId,
|
|
282
|
+
user_id: userId,
|
|
283
|
+
started_at: { $ne: null },
|
|
284
|
+
stopped_at: null
|
|
285
|
+
});
|
|
286
|
+
if (!active) {
|
|
287
|
+
throw Object.assign(
|
|
288
|
+
new Error("No active timer found for this issue."),
|
|
289
|
+
{ status: 404 }
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const now = /* @__PURE__ */ new Date();
|
|
293
|
+
const durationMs = now.getTime() - new Date(active.started_at).getTime();
|
|
294
|
+
const durationMinutes = Math.max(1, Math.round(durationMs / 6e4));
|
|
295
|
+
active.stopped_at = now;
|
|
296
|
+
active.duration_minutes = durationMinutes;
|
|
297
|
+
active.logged_date = now;
|
|
298
|
+
await repo.persistAndFlush(active);
|
|
299
|
+
return active;
|
|
300
|
+
}
|
|
301
|
+
/** Return the running timer entry for a user on an issue, or null if none. */
|
|
302
|
+
async getActiveTimer(issueId, userId) {
|
|
303
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
304
|
+
return repo.findOne({
|
|
305
|
+
issue_id: issueId,
|
|
306
|
+
user_id: userId,
|
|
307
|
+
started_at: { $ne: null },
|
|
308
|
+
stopped_at: null
|
|
309
|
+
}) ?? null;
|
|
310
|
+
}
|
|
311
|
+
/** Delete a time log entry by ID. */
|
|
312
|
+
async deleteTimeLog(id) {
|
|
313
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
314
|
+
const entry = await repo.findOne({ id });
|
|
315
|
+
if (!entry) {
|
|
316
|
+
throw Object.assign(new Error(`Time log entry ${id} not found`), { status: 404 });
|
|
317
|
+
}
|
|
318
|
+
await repo.removeAndFlush(entry);
|
|
319
|
+
return entry;
|
|
320
|
+
}
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Task Lists
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
/** List all task lists for a project, ordered by position. */
|
|
325
|
+
async listTaskListsByProject(projectId) {
|
|
326
|
+
const repo = this.container.resolve("taskListRepository");
|
|
327
|
+
return repo.find({ project_id: projectId }, { orderBy: { position: "ASC" } });
|
|
328
|
+
}
|
|
329
|
+
/** Create a task list for a project. Position defaults to max+1. */
|
|
330
|
+
async createTaskList(input) {
|
|
331
|
+
const repo = this.container.resolve("taskListRepository");
|
|
332
|
+
const existing = await repo.find({ project_id: input.project_id });
|
|
333
|
+
const maxPos = existing.reduce((m, tl) => Math.max(m, tl.position ?? 0), -1);
|
|
334
|
+
const taskList = repo.create({ ...input, position: maxPos + 1 });
|
|
335
|
+
await repo.persistAndFlush(taskList);
|
|
336
|
+
return taskList;
|
|
337
|
+
}
|
|
338
|
+
/** Update a task list's name or description. */
|
|
339
|
+
async updateTaskList(id, data) {
|
|
340
|
+
const repo = this.container.resolve("taskListRepository");
|
|
341
|
+
const taskList = await repo.findOne({ id });
|
|
342
|
+
if (!taskList) throw Object.assign(new Error(`TaskList ${id} not found`), { status: 404 });
|
|
343
|
+
Object.assign(taskList, data);
|
|
344
|
+
await repo.persistAndFlush(taskList);
|
|
345
|
+
return taskList;
|
|
346
|
+
}
|
|
347
|
+
/** Delete a task list by ID. Clears task_list_id on associated issues. */
|
|
348
|
+
async deleteTaskList(id) {
|
|
349
|
+
const taskListRepo = this.container.resolve("taskListRepository");
|
|
350
|
+
const issueRepo = this.container.resolve("issueRepository");
|
|
351
|
+
const taskList = await taskListRepo.findOne({ id });
|
|
352
|
+
if (!taskList) throw Object.assign(new Error(`TaskList ${id} not found`), { status: 404 });
|
|
353
|
+
const issues = await issueRepo.find({ task_list_id: id });
|
|
354
|
+
for (const issue of issues) {
|
|
355
|
+
issue.task_list_id = null;
|
|
356
|
+
}
|
|
357
|
+
if (issues.length > 0) await issueRepo.flush();
|
|
358
|
+
await taskListRepo.removeAndFlush(taskList);
|
|
359
|
+
return taskList;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/loaders/default.ts
|
|
364
|
+
var import_framework_utils7 = require("@meridianjs/framework-utils");
|
|
365
|
+
var IssueSchema = (0, import_framework_utils7.dmlToEntitySchema)(issue_default);
|
|
366
|
+
var CommentSchema = (0, import_framework_utils7.dmlToEntitySchema)(comment_default);
|
|
367
|
+
var AttachmentSchema = (0, import_framework_utils7.dmlToEntitySchema)(attachment_default);
|
|
368
|
+
var TimeLogSchema = (0, import_framework_utils7.dmlToEntitySchema)(time_log_default);
|
|
369
|
+
var TaskListSchema = (0, import_framework_utils7.dmlToEntitySchema)(task_list_default);
|
|
370
|
+
var entitySchemas = [IssueSchema, CommentSchema, AttachmentSchema, TimeLogSchema, TaskListSchema];
|
|
371
|
+
async function defaultLoader({ container }) {
|
|
372
|
+
const config = container.resolve("config");
|
|
373
|
+
const { databaseUrl } = config.projectConfig;
|
|
374
|
+
const orm = await (0, import_framework_utils7.createModuleOrm)(entitySchemas, databaseUrl);
|
|
375
|
+
const em = orm.em.fork();
|
|
376
|
+
container.register({
|
|
377
|
+
issueRepository: (0, import_framework_utils7.createRepository)(em, "issue"),
|
|
378
|
+
commentRepository: (0, import_framework_utils7.createRepository)(em, "comment"),
|
|
379
|
+
attachmentRepository: (0, import_framework_utils7.createRepository)(em, "attachment"),
|
|
380
|
+
timeLogRepository: (0, import_framework_utils7.createRepository)(em, "time_log"),
|
|
381
|
+
taskListRepository: (0, import_framework_utils7.createRepository)(em, "task_list"),
|
|
382
|
+
issueOrm: orm
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/index.ts
|
|
387
|
+
var ISSUE_MODULE = "issueModuleService";
|
|
388
|
+
var index_default = (0, import_framework_utils8.Module)(ISSUE_MODULE, {
|
|
389
|
+
service: IssueModuleService,
|
|
390
|
+
models: [issue_default, comment_default, attachment_default, time_log_default, task_list_default],
|
|
391
|
+
loaders: [defaultLoader],
|
|
392
|
+
linkable: {
|
|
393
|
+
issue: { tableName: "issue", primaryKey: "id" },
|
|
394
|
+
comment: { tableName: "comment", primaryKey: "id" },
|
|
395
|
+
attachment: { tableName: "attachment", primaryKey: "id" },
|
|
396
|
+
time_log: { tableName: "time_log", primaryKey: "id" },
|
|
397
|
+
task_list: { tableName: "task_list", primaryKey: "id" }
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
401
|
+
0 && (module.exports = {
|
|
402
|
+
ISSUE_MODULE,
|
|
403
|
+
IssueModuleService
|
|
404
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Module } from "@meridianjs/framework-utils";
|
|
3
|
+
|
|
4
|
+
// src/service.ts
|
|
5
|
+
import { MeridianService } from "@meridianjs/framework-utils";
|
|
6
|
+
|
|
7
|
+
// src/models/issue.ts
|
|
8
|
+
import { model } from "@meridianjs/framework-utils";
|
|
9
|
+
var Issue = model.define("issue", {
|
|
10
|
+
id: model.id().primaryKey(),
|
|
11
|
+
/** Full identifier, e.g. "PROJ-42" */
|
|
12
|
+
identifier: model.text(),
|
|
13
|
+
/** Sequential number within the project, e.g. 42 */
|
|
14
|
+
number: model.number(),
|
|
15
|
+
title: model.text(),
|
|
16
|
+
description: model.text().nullable(),
|
|
17
|
+
type: model.enum(["bug", "feature", "task", "epic", "story"]).default("task"),
|
|
18
|
+
priority: model.enum(["urgent", "high", "medium", "low", "none"]).default("none"),
|
|
19
|
+
status: model.text().default("backlog"),
|
|
20
|
+
/** Denormalized — not a FK */
|
|
21
|
+
project_id: model.text(),
|
|
22
|
+
workspace_id: model.text(),
|
|
23
|
+
/** Array of user IDs assigned to this issue */
|
|
24
|
+
assignee_ids: model.json().nullable(),
|
|
25
|
+
reporter_id: model.text().nullable(),
|
|
26
|
+
/** Parent issue ID for subtasks */
|
|
27
|
+
parent_id: model.text().nullable(),
|
|
28
|
+
/** Denormalized sprint reference — no FK constraint */
|
|
29
|
+
sprint_id: model.text().nullable(),
|
|
30
|
+
/** Denormalized task list reference — no FK constraint */
|
|
31
|
+
task_list_id: model.text().nullable(),
|
|
32
|
+
due_date: model.date().nullable(),
|
|
33
|
+
/** Story point estimate */
|
|
34
|
+
estimate: model.number().nullable()
|
|
35
|
+
}, [
|
|
36
|
+
{ columns: ["project_id"] },
|
|
37
|
+
{ columns: ["workspace_id"] },
|
|
38
|
+
{ columns: ["project_id", "status"] }
|
|
39
|
+
]);
|
|
40
|
+
var issue_default = Issue;
|
|
41
|
+
|
|
42
|
+
// src/models/comment.ts
|
|
43
|
+
import { model as model2 } from "@meridianjs/framework-utils";
|
|
44
|
+
var Comment = model2.define("comment", {
|
|
45
|
+
id: model2.id().primaryKey(),
|
|
46
|
+
body: model2.text(),
|
|
47
|
+
issue_id: model2.text(),
|
|
48
|
+
author_id: model2.text(),
|
|
49
|
+
edited_at: model2.date().nullable()
|
|
50
|
+
});
|
|
51
|
+
var comment_default = Comment;
|
|
52
|
+
|
|
53
|
+
// src/models/attachment.ts
|
|
54
|
+
import { model as model3 } from "@meridianjs/framework-utils";
|
|
55
|
+
var Attachment = model3.define("attachment", {
|
|
56
|
+
id: model3.id().primaryKey(),
|
|
57
|
+
issue_id: model3.text(),
|
|
58
|
+
/** Set when the attachment belongs to a specific comment; null for issue-level attachments */
|
|
59
|
+
comment_id: model3.text().nullable(),
|
|
60
|
+
/** UUID-based stored filename on disk */
|
|
61
|
+
filename: model3.text(),
|
|
62
|
+
/** Original filename from the upload */
|
|
63
|
+
original_name: model3.text(),
|
|
64
|
+
mime_type: model3.text(),
|
|
65
|
+
/** File size in bytes */
|
|
66
|
+
size: model3.number(),
|
|
67
|
+
/** Publicly accessible URL, e.g. /uploads/issue-attachments/<uuid>-<name> */
|
|
68
|
+
url: model3.text(),
|
|
69
|
+
uploader_id: model3.text(),
|
|
70
|
+
workspace_id: model3.text()
|
|
71
|
+
}, [
|
|
72
|
+
{ columns: ["issue_id"] },
|
|
73
|
+
{ columns: ["comment_id"] }
|
|
74
|
+
]);
|
|
75
|
+
var attachment_default = Attachment;
|
|
76
|
+
|
|
77
|
+
// src/models/time-log.ts
|
|
78
|
+
import { model as model4 } from "@meridianjs/framework-utils";
|
|
79
|
+
var TimeLog = model4.define("time_log", {
|
|
80
|
+
id: model4.id().primaryKey(),
|
|
81
|
+
issue_id: model4.text(),
|
|
82
|
+
user_id: model4.text(),
|
|
83
|
+
workspace_id: model4.text(),
|
|
84
|
+
/** Total duration in minutes. null when a timer is still running. */
|
|
85
|
+
duration_minutes: model4.number().nullable(),
|
|
86
|
+
description: model4.text().nullable(),
|
|
87
|
+
/** The calendar date for this time entry (manual entries) */
|
|
88
|
+
logged_date: model4.date().nullable(),
|
|
89
|
+
/** Set when the timer starts; null for manual entries */
|
|
90
|
+
started_at: model4.date().nullable(),
|
|
91
|
+
/** Set when the timer stops; null while timer is running */
|
|
92
|
+
stopped_at: model4.date().nullable(),
|
|
93
|
+
source: model4.enum(["manual", "timer"]).default("manual")
|
|
94
|
+
}, [
|
|
95
|
+
{ columns: ["issue_id"] },
|
|
96
|
+
{ columns: ["user_id"] }
|
|
97
|
+
]);
|
|
98
|
+
var time_log_default = TimeLog;
|
|
99
|
+
|
|
100
|
+
// src/models/task-list.ts
|
|
101
|
+
import { model as model5 } from "@meridianjs/framework-utils";
|
|
102
|
+
var TaskList = model5.define("task_list", {
|
|
103
|
+
id: model5.id().primaryKey(),
|
|
104
|
+
name: model5.text(),
|
|
105
|
+
description: model5.text().nullable(),
|
|
106
|
+
/** Denormalized — not a FK */
|
|
107
|
+
project_id: model5.text(),
|
|
108
|
+
position: model5.number().default(0)
|
|
109
|
+
}, [
|
|
110
|
+
{ columns: ["project_id"] }
|
|
111
|
+
]);
|
|
112
|
+
var task_list_default = TaskList;
|
|
113
|
+
|
|
114
|
+
// src/service.ts
|
|
115
|
+
var IssueModuleService = class extends MeridianService({
|
|
116
|
+
Issue: issue_default,
|
|
117
|
+
Comment: comment_default,
|
|
118
|
+
Attachment: attachment_default,
|
|
119
|
+
TimeLog: time_log_default,
|
|
120
|
+
TaskList: task_list_default
|
|
121
|
+
}) {
|
|
122
|
+
container;
|
|
123
|
+
constructor(container) {
|
|
124
|
+
super(container);
|
|
125
|
+
this.container = container;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create an issue with an auto-generated sequential identifier (e.g. PROJ-42).
|
|
129
|
+
* Looks up the project to get its identifier prefix, then assigns the next number.
|
|
130
|
+
*/
|
|
131
|
+
async createIssueInProject(input) {
|
|
132
|
+
const issueRepo = this.container.resolve("issueRepository");
|
|
133
|
+
const projectService = this.container.resolve("projectModuleService");
|
|
134
|
+
const project = await projectService.retrieveProject(input.project_id);
|
|
135
|
+
if (!project) {
|
|
136
|
+
throw Object.assign(new Error(`Project ${input.project_id} not found`), { status: 404 });
|
|
137
|
+
}
|
|
138
|
+
const existing = await issueRepo.find({ project_id: input.project_id });
|
|
139
|
+
const maxNumber = existing.reduce(
|
|
140
|
+
(max, issue2) => Math.max(max, issue2.number ?? 0),
|
|
141
|
+
0
|
|
142
|
+
);
|
|
143
|
+
const nextNumber = maxNumber + 1;
|
|
144
|
+
const identifier = `${project.identifier}-${nextNumber}`;
|
|
145
|
+
const issue = issueRepo.create({
|
|
146
|
+
...input,
|
|
147
|
+
number: nextNumber,
|
|
148
|
+
identifier,
|
|
149
|
+
type: input.type ?? "task",
|
|
150
|
+
priority: input.priority ?? "none",
|
|
151
|
+
status: input.status ?? "backlog"
|
|
152
|
+
});
|
|
153
|
+
await issueRepo.persistAndFlush(issue);
|
|
154
|
+
return issue;
|
|
155
|
+
}
|
|
156
|
+
/** List issues for a project with optional filters. */
|
|
157
|
+
async listIssuesByProject(projectId, filters = {}, options = {}) {
|
|
158
|
+
const repo = this.container.resolve("issueRepository");
|
|
159
|
+
return repo.find(
|
|
160
|
+
{ project_id: projectId, ...filters },
|
|
161
|
+
{ limit: options.limit ?? 50, offset: options.offset ?? 0 }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
/** List all comments for an issue. */
|
|
165
|
+
async listCommentsByIssue(issueId) {
|
|
166
|
+
const repo = this.container.resolve("commentRepository");
|
|
167
|
+
return repo.find({ issue_id: issueId });
|
|
168
|
+
}
|
|
169
|
+
/** Add a comment to an issue. */
|
|
170
|
+
async createComment(input) {
|
|
171
|
+
const repo = this.container.resolve("commentRepository");
|
|
172
|
+
const comment = repo.create(input);
|
|
173
|
+
await repo.persistAndFlush(comment);
|
|
174
|
+
return comment;
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Attachments
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/** List all attachments for an issue, ordered by creation date. */
|
|
180
|
+
async listAttachmentsByIssue(issueId) {
|
|
181
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
182
|
+
return repo.find({ issue_id: issueId }, { orderBy: { created_at: "ASC" } });
|
|
183
|
+
}
|
|
184
|
+
/** Persist an attachment record after the file has been stored on disk. */
|
|
185
|
+
async createAttachment(input) {
|
|
186
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
187
|
+
const attachment = repo.create(input);
|
|
188
|
+
await repo.persistAndFlush(attachment);
|
|
189
|
+
return attachment;
|
|
190
|
+
}
|
|
191
|
+
/** Delete an attachment record by ID. The caller is responsible for removing the file. */
|
|
192
|
+
async deleteAttachment(attachmentId) {
|
|
193
|
+
const repo = this.container.resolve("attachmentRepository");
|
|
194
|
+
const attachment = await repo.findOne({ id: attachmentId });
|
|
195
|
+
if (!attachment) {
|
|
196
|
+
throw Object.assign(new Error(`Attachment ${attachmentId} not found`), { status: 404 });
|
|
197
|
+
}
|
|
198
|
+
await repo.removeAndFlush(attachment);
|
|
199
|
+
return attachment;
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Time Logging
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
/** List all time log entries for an issue, newest first. */
|
|
205
|
+
async listTimeLogsByIssue(issueId) {
|
|
206
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
207
|
+
return repo.find({ issue_id: issueId }, { orderBy: { created_at: "DESC" } });
|
|
208
|
+
}
|
|
209
|
+
/** Create a manual time log entry with an explicit duration. */
|
|
210
|
+
async createManualTimeLog(input) {
|
|
211
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
212
|
+
const entry = repo.create({
|
|
213
|
+
...input,
|
|
214
|
+
source: "manual",
|
|
215
|
+
logged_date: input.logged_date ?? /* @__PURE__ */ new Date()
|
|
216
|
+
});
|
|
217
|
+
await repo.persistAndFlush(entry);
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Start a timer for a user on an issue.
|
|
222
|
+
* Throws if the user already has an active timer on this issue.
|
|
223
|
+
*/
|
|
224
|
+
async startTimer(issueId, userId, workspaceId) {
|
|
225
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
226
|
+
const active = await repo.findOne({
|
|
227
|
+
issue_id: issueId,
|
|
228
|
+
user_id: userId,
|
|
229
|
+
started_at: { $ne: null },
|
|
230
|
+
stopped_at: null
|
|
231
|
+
});
|
|
232
|
+
if (active) {
|
|
233
|
+
throw Object.assign(
|
|
234
|
+
new Error("A timer is already running for this issue. Stop it before starting a new one."),
|
|
235
|
+
{ status: 409 }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const entry = repo.create({
|
|
239
|
+
issue_id: issueId,
|
|
240
|
+
user_id: userId,
|
|
241
|
+
workspace_id: workspaceId,
|
|
242
|
+
source: "timer",
|
|
243
|
+
started_at: /* @__PURE__ */ new Date()
|
|
244
|
+
});
|
|
245
|
+
await repo.persistAndFlush(entry);
|
|
246
|
+
return entry;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Stop the active timer for a user on an issue.
|
|
250
|
+
* Calculates duration from started_at and finalises the entry.
|
|
251
|
+
*/
|
|
252
|
+
async stopTimer(issueId, userId) {
|
|
253
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
254
|
+
const active = await repo.findOne({
|
|
255
|
+
issue_id: issueId,
|
|
256
|
+
user_id: userId,
|
|
257
|
+
started_at: { $ne: null },
|
|
258
|
+
stopped_at: null
|
|
259
|
+
});
|
|
260
|
+
if (!active) {
|
|
261
|
+
throw Object.assign(
|
|
262
|
+
new Error("No active timer found for this issue."),
|
|
263
|
+
{ status: 404 }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const now = /* @__PURE__ */ new Date();
|
|
267
|
+
const durationMs = now.getTime() - new Date(active.started_at).getTime();
|
|
268
|
+
const durationMinutes = Math.max(1, Math.round(durationMs / 6e4));
|
|
269
|
+
active.stopped_at = now;
|
|
270
|
+
active.duration_minutes = durationMinutes;
|
|
271
|
+
active.logged_date = now;
|
|
272
|
+
await repo.persistAndFlush(active);
|
|
273
|
+
return active;
|
|
274
|
+
}
|
|
275
|
+
/** Return the running timer entry for a user on an issue, or null if none. */
|
|
276
|
+
async getActiveTimer(issueId, userId) {
|
|
277
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
278
|
+
return repo.findOne({
|
|
279
|
+
issue_id: issueId,
|
|
280
|
+
user_id: userId,
|
|
281
|
+
started_at: { $ne: null },
|
|
282
|
+
stopped_at: null
|
|
283
|
+
}) ?? null;
|
|
284
|
+
}
|
|
285
|
+
/** Delete a time log entry by ID. */
|
|
286
|
+
async deleteTimeLog(id) {
|
|
287
|
+
const repo = this.container.resolve("timeLogRepository");
|
|
288
|
+
const entry = await repo.findOne({ id });
|
|
289
|
+
if (!entry) {
|
|
290
|
+
throw Object.assign(new Error(`Time log entry ${id} not found`), { status: 404 });
|
|
291
|
+
}
|
|
292
|
+
await repo.removeAndFlush(entry);
|
|
293
|
+
return entry;
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Task Lists
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
/** List all task lists for a project, ordered by position. */
|
|
299
|
+
async listTaskListsByProject(projectId) {
|
|
300
|
+
const repo = this.container.resolve("taskListRepository");
|
|
301
|
+
return repo.find({ project_id: projectId }, { orderBy: { position: "ASC" } });
|
|
302
|
+
}
|
|
303
|
+
/** Create a task list for a project. Position defaults to max+1. */
|
|
304
|
+
async createTaskList(input) {
|
|
305
|
+
const repo = this.container.resolve("taskListRepository");
|
|
306
|
+
const existing = await repo.find({ project_id: input.project_id });
|
|
307
|
+
const maxPos = existing.reduce((m, tl) => Math.max(m, tl.position ?? 0), -1);
|
|
308
|
+
const taskList = repo.create({ ...input, position: maxPos + 1 });
|
|
309
|
+
await repo.persistAndFlush(taskList);
|
|
310
|
+
return taskList;
|
|
311
|
+
}
|
|
312
|
+
/** Update a task list's name or description. */
|
|
313
|
+
async updateTaskList(id, data) {
|
|
314
|
+
const repo = this.container.resolve("taskListRepository");
|
|
315
|
+
const taskList = await repo.findOne({ id });
|
|
316
|
+
if (!taskList) throw Object.assign(new Error(`TaskList ${id} not found`), { status: 404 });
|
|
317
|
+
Object.assign(taskList, data);
|
|
318
|
+
await repo.persistAndFlush(taskList);
|
|
319
|
+
return taskList;
|
|
320
|
+
}
|
|
321
|
+
/** Delete a task list by ID. Clears task_list_id on associated issues. */
|
|
322
|
+
async deleteTaskList(id) {
|
|
323
|
+
const taskListRepo = this.container.resolve("taskListRepository");
|
|
324
|
+
const issueRepo = this.container.resolve("issueRepository");
|
|
325
|
+
const taskList = await taskListRepo.findOne({ id });
|
|
326
|
+
if (!taskList) throw Object.assign(new Error(`TaskList ${id} not found`), { status: 404 });
|
|
327
|
+
const issues = await issueRepo.find({ task_list_id: id });
|
|
328
|
+
for (const issue of issues) {
|
|
329
|
+
issue.task_list_id = null;
|
|
330
|
+
}
|
|
331
|
+
if (issues.length > 0) await issueRepo.flush();
|
|
332
|
+
await taskListRepo.removeAndFlush(taskList);
|
|
333
|
+
return taskList;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/loaders/default.ts
|
|
338
|
+
import { dmlToEntitySchema, createRepository, createModuleOrm } from "@meridianjs/framework-utils";
|
|
339
|
+
var IssueSchema = dmlToEntitySchema(issue_default);
|
|
340
|
+
var CommentSchema = dmlToEntitySchema(comment_default);
|
|
341
|
+
var AttachmentSchema = dmlToEntitySchema(attachment_default);
|
|
342
|
+
var TimeLogSchema = dmlToEntitySchema(time_log_default);
|
|
343
|
+
var TaskListSchema = dmlToEntitySchema(task_list_default);
|
|
344
|
+
var entitySchemas = [IssueSchema, CommentSchema, AttachmentSchema, TimeLogSchema, TaskListSchema];
|
|
345
|
+
async function defaultLoader({ container }) {
|
|
346
|
+
const config = container.resolve("config");
|
|
347
|
+
const { databaseUrl } = config.projectConfig;
|
|
348
|
+
const orm = await createModuleOrm(entitySchemas, databaseUrl);
|
|
349
|
+
const em = orm.em.fork();
|
|
350
|
+
container.register({
|
|
351
|
+
issueRepository: createRepository(em, "issue"),
|
|
352
|
+
commentRepository: createRepository(em, "comment"),
|
|
353
|
+
attachmentRepository: createRepository(em, "attachment"),
|
|
354
|
+
timeLogRepository: createRepository(em, "time_log"),
|
|
355
|
+
taskListRepository: createRepository(em, "task_list"),
|
|
356
|
+
issueOrm: orm
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/index.ts
|
|
361
|
+
var ISSUE_MODULE = "issueModuleService";
|
|
362
|
+
var index_default = Module(ISSUE_MODULE, {
|
|
363
|
+
service: IssueModuleService,
|
|
364
|
+
models: [issue_default, comment_default, attachment_default, time_log_default, task_list_default],
|
|
365
|
+
loaders: [defaultLoader],
|
|
366
|
+
linkable: {
|
|
367
|
+
issue: { tableName: "issue", primaryKey: "id" },
|
|
368
|
+
comment: { tableName: "comment", primaryKey: "id" },
|
|
369
|
+
attachment: { tableName: "attachment", primaryKey: "id" },
|
|
370
|
+
time_log: { tableName: "time_log", primaryKey: "id" },
|
|
371
|
+
task_list: { tableName: "task_list", primaryKey: "id" }
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
export {
|
|
375
|
+
ISSUE_MODULE,
|
|
376
|
+
IssueModuleService,
|
|
377
|
+
index_default as default
|
|
378
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meridianjs/issue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Meridian issue module — Issue and Comment domain models",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
22
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@meridianjs/framework-utils": "^0.1.0",
|
|
29
|
+
"@meridianjs/types": "^0.1.0",
|
|
30
|
+
"@mikro-orm/core": "^6.4.3",
|
|
31
|
+
"@mikro-orm/postgresql": "^6.4.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"tsup": "^8.3.5",
|
|
35
|
+
"typescript": "*"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|