@mj-biz-apps/issues-core-entities-server 0.0.1 → 1.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.
@@ -0,0 +1,53 @@
1
+ import { EntitySaveOptions } from '@memberjunction/core';
2
+ import { mjBizAppsIssuesIssueEntity } from '@mj-biz-apps/issues-entities';
3
+ /**
4
+ * Server-side subclass of {@link mjBizAppsIssuesIssueEntity}.
5
+ *
6
+ * Owns the SERVER-ONLY lifecycle side-effects that must fire on EVERY save path
7
+ * (UI, API, automation) — not just when callers route through IssueService:
8
+ *
9
+ * 1. **IssueNumber assignment** (insert only) — via the atomic
10
+ * spAssignNextIssueNumber proc, before super.Save(); immutable thereafter.
11
+ * 2. **Lifecycle timestamp stamping** (in Save(), before super.Save()) — when
12
+ * StatusID changes: stamp ResolvedAt on entering a Resolved/Terminal status,
13
+ * ClosedAt on entering a Terminal status, and clear them on reopen. These
14
+ * mutate the row being written, so they happen inline.
15
+ * 3. **IssueType action hooks** (post-save, via RegisterEventHandler) — fire
16
+ * OnCreate (insert), OnStatusChange (+ OnClose on terminal), and OnAssign
17
+ * (assignee change) through the Action engine. These are decoupled reactions
18
+ * that must NOT be able to fail or slow the save, so they run after the save
19
+ * event finalizes — mirroring bizapps-tasks' TaskNotificationHandler and
20
+ * SaaS's OrganizationPersonRoleEntityServer event-handler pattern.
21
+ *
22
+ * Registered at priority 2 so this server subclass wins over the priority-1
23
+ * client-shared entity.
24
+ */
25
+ export declare class IssueEntityServer extends mjBizAppsIssuesIssueEntity {
26
+ private _hookHandlerRegistered;
27
+ Save(options?: EntitySaveOptions): Promise<boolean>;
28
+ /** Allocates and sets IssueNumber for a new Issue based on its AppScope. */
29
+ private assignIssueNumber;
30
+ /**
31
+ * Stamps ResolvedAt / ClosedAt based on the NEW status's flags:
32
+ * - entering an IsResolved OR IsTerminal status → set ResolvedAt (if not already set)
33
+ * - entering an IsTerminal status → set ClosedAt
34
+ * - leaving into a non-terminal, non-resolved (active) status (reopen) → clear both
35
+ * Loads the engine for the IsResolved/IsTerminal lookup.
36
+ */
37
+ private stampLifecycleTimestamps;
38
+ private _pendingHookContext;
39
+ /**
40
+ * Registers a per-instance save-event handler (once) that fires the IssueType
41
+ * action hooks AFTER the save finalizes. Fire-and-forget; failures are logged,
42
+ * never propagated — a misconfigured hook must not break the save.
43
+ */
44
+ private ensureHookHandler;
45
+ /** Dispatches the IssueType action hooks for the events that occurred this save. */
46
+ private fireLifecycleHooks;
47
+ /**
48
+ * Fires a single IssueType action hook by Action ID, passing the issue's identity
49
+ * as input params. No-op when actionID is null. Failures are logged, not thrown.
50
+ */
51
+ private fireHook;
52
+ }
53
+ //# sourceMappingURL=IssueEntityServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IssueEntityServer.d.ts","sourceRoot":"","sources":["../src/IssueEntityServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,iBAAiB,EAAsB,MAAM,sBAAsB,CAAC;AAI1G,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAK1E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBACa,iBAAkB,SAAQ,0BAA0B;IAC/D,OAAO,CAAC,sBAAsB,CAAS;IAEjB,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAmCzE,4EAA4E;YAC9D,iBAAiB;IAc/B;;;;;;OAMG;YACW,wBAAwB;IA6BtC,OAAO,CAAC,mBAAmB,CAA4B;IAEvD;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAsBzB,oFAAoF;YACtE,kBAAkB;IAqBhC;;;OAGG;YACW,QAAQ;CAiCvB"}
@@ -0,0 +1,201 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseEntity, LogError } from '@memberjunction/core';
8
+ import { RegisterClass } from '@memberjunction/global';
9
+ import { ActionEngineServer } from '@memberjunction/actions';
10
+ import { mjBizAppsIssuesIssueEntity } from '@mj-biz-apps/issues-entities';
11
+ import { IssueEngine } from '@mj-biz-apps/issues-core';
12
+ import { SequenceService } from './SequenceService.js';
13
+ /**
14
+ * Server-side subclass of {@link mjBizAppsIssuesIssueEntity}.
15
+ *
16
+ * Owns the SERVER-ONLY lifecycle side-effects that must fire on EVERY save path
17
+ * (UI, API, automation) — not just when callers route through IssueService:
18
+ *
19
+ * 1. **IssueNumber assignment** (insert only) — via the atomic
20
+ * spAssignNextIssueNumber proc, before super.Save(); immutable thereafter.
21
+ * 2. **Lifecycle timestamp stamping** (in Save(), before super.Save()) — when
22
+ * StatusID changes: stamp ResolvedAt on entering a Resolved/Terminal status,
23
+ * ClosedAt on entering a Terminal status, and clear them on reopen. These
24
+ * mutate the row being written, so they happen inline.
25
+ * 3. **IssueType action hooks** (post-save, via RegisterEventHandler) — fire
26
+ * OnCreate (insert), OnStatusChange (+ OnClose on terminal), and OnAssign
27
+ * (assignee change) through the Action engine. These are decoupled reactions
28
+ * that must NOT be able to fail or slow the save, so they run after the save
29
+ * event finalizes — mirroring bizapps-tasks' TaskNotificationHandler and
30
+ * SaaS's OrganizationPersonRoleEntityServer event-handler pattern.
31
+ *
32
+ * Registered at priority 2 so this server subclass wins over the priority-1
33
+ * client-shared entity.
34
+ */
35
+ let IssueEntityServer = class IssueEntityServer extends mjBizAppsIssuesIssueEntity {
36
+ constructor() {
37
+ super(...arguments);
38
+ this._hookHandlerRegistered = false;
39
+ // ------------------------------------------------------------------
40
+ // 3. Action hooks (post-save reaction via event handler)
41
+ // ------------------------------------------------------------------
42
+ this._pendingHookContext = null;
43
+ }
44
+ async Save(options) {
45
+ this.ensureHookHandler();
46
+ const isNew = !this.IsSaved;
47
+ // Snapshot the status transition BEFORE super.Save() resets dirty flags.
48
+ const statusField = this.GetFieldByName('StatusID');
49
+ const statusChanged = statusField?.Dirty ?? false;
50
+ const oldStatusID = statusField?.OldValue ?? null;
51
+ const assigneeField = this.GetFieldByName('AssigneeRecordID');
52
+ const assigneeEntityField = this.GetFieldByName('AssigneeEntityID');
53
+ const assigneeChanged = (assigneeField?.Dirty ?? false) || (assigneeEntityField?.Dirty ?? false);
54
+ // 1. IssueNumber (insert only, immutable)
55
+ if (isNew && !this.IssueNumber) {
56
+ await this.assignIssueNumber();
57
+ }
58
+ // 2. Lifecycle timestamps — stamp inline so they're part of this write.
59
+ // On insert, treat the initial status as "entered" too.
60
+ if (statusChanged || isNew) {
61
+ await this.stampLifecycleTimestamps();
62
+ }
63
+ // Stash what the post-save hook handler needs (dirty flags are gone after save).
64
+ this._pendingHookContext = { isNew, statusChanged: statusChanged && !isNew, oldStatusID, assigneeChanged: assigneeChanged && !isNew };
65
+ return super.Save(options);
66
+ }
67
+ // ------------------------------------------------------------------
68
+ // 1. IssueNumber assignment
69
+ // ------------------------------------------------------------------
70
+ /** Allocates and sets IssueNumber for a new Issue based on its AppScope. */
71
+ async assignIssueNumber() {
72
+ try {
73
+ this.IssueNumber = await SequenceService.assignNextIssueNumber(this.AppScope, this);
74
+ }
75
+ catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err);
77
+ LogError(`IssueEntityServer: failed to assign IssueNumber (AppScope=${this.AppScope ?? '(null)'}): ${msg}`);
78
+ throw err;
79
+ }
80
+ }
81
+ // ------------------------------------------------------------------
82
+ // 2. Lifecycle timestamp stamping (inline, mutates the row)
83
+ // ------------------------------------------------------------------
84
+ /**
85
+ * Stamps ResolvedAt / ClosedAt based on the NEW status's flags:
86
+ * - entering an IsResolved OR IsTerminal status → set ResolvedAt (if not already set)
87
+ * - entering an IsTerminal status → set ClosedAt
88
+ * - leaving into a non-terminal, non-resolved (active) status (reopen) → clear both
89
+ * Loads the engine for the IsResolved/IsTerminal lookup.
90
+ */
91
+ async stampLifecycleTimestamps() {
92
+ await IssueEngine.Instance.Config(false, this.ContextCurrentUser);
93
+ const isResolved = IssueEngine.Instance.IsResolvedStatus(this.StatusID);
94
+ const isTerminal = IssueEngine.Instance.IsTerminalStatus(this.StatusID);
95
+ const now = new Date();
96
+ if (isTerminal) {
97
+ if (!this.ResolvedAt) {
98
+ this.ResolvedAt = now;
99
+ }
100
+ this.ClosedAt = now;
101
+ }
102
+ else if (isResolved) {
103
+ if (!this.ResolvedAt) {
104
+ this.ResolvedAt = now;
105
+ }
106
+ // Resolved-but-not-closed: ensure not marked closed.
107
+ this.ClosedAt = null;
108
+ }
109
+ else {
110
+ // Active (reopened or never resolved): clear lifecycle stamps.
111
+ this.ResolvedAt = null;
112
+ this.ClosedAt = null;
113
+ }
114
+ }
115
+ /**
116
+ * Registers a per-instance save-event handler (once) that fires the IssueType
117
+ * action hooks AFTER the save finalizes. Fire-and-forget; failures are logged,
118
+ * never propagated — a misconfigured hook must not break the save.
119
+ */
120
+ ensureHookHandler() {
121
+ if (this._hookHandlerRegistered) {
122
+ return;
123
+ }
124
+ this._hookHandlerRegistered = true;
125
+ this.RegisterEventHandler((event) => {
126
+ if (event.type !== 'save') {
127
+ return;
128
+ }
129
+ const ctx = this._pendingHookContext;
130
+ this._pendingHookContext = null;
131
+ if (!ctx) {
132
+ return;
133
+ }
134
+ this.fireLifecycleHooks(ctx).catch((err) => {
135
+ const msg = err instanceof Error ? err.message : String(err);
136
+ LogError(`IssueEntityServer: lifecycle hook firing failed for issue ${this.ID}: ${msg}`);
137
+ });
138
+ });
139
+ }
140
+ /** Dispatches the IssueType action hooks for the events that occurred this save. */
141
+ async fireLifecycleHooks(ctx) {
142
+ await IssueEngine.Instance.Config(false, this.ContextCurrentUser);
143
+ const issueType = IssueEngine.Instance.IssueTypeByID(this.IssueTypeID);
144
+ if (!issueType) {
145
+ return;
146
+ }
147
+ if (ctx.isNew) {
148
+ await this.fireHook(issueType.OnCreateActionID);
149
+ }
150
+ if (ctx.statusChanged) {
151
+ await this.fireHook(issueType.OnStatusChangeActionID);
152
+ if (IssueEngine.Instance.IsTerminalStatus(this.StatusID)) {
153
+ await this.fireHook(issueType.OnCloseActionID);
154
+ }
155
+ }
156
+ if (ctx.assigneeChanged) {
157
+ await this.fireHook(issueType.OnAssignActionID);
158
+ }
159
+ }
160
+ /**
161
+ * Fires a single IssueType action hook by Action ID, passing the issue's identity
162
+ * as input params. No-op when actionID is null. Failures are logged, not thrown.
163
+ */
164
+ async fireHook(actionID) {
165
+ if (!actionID) {
166
+ return;
167
+ }
168
+ try {
169
+ const contextUser = this.ContextCurrentUser;
170
+ await ActionEngineServer.Instance.Config(false, contextUser);
171
+ const action = ActionEngineServer.Instance.Actions.find((a) => a.ID === actionID);
172
+ if (!action) {
173
+ LogError(`IssueEntityServer.fireHook: Action ${actionID} not found`);
174
+ return;
175
+ }
176
+ const params = [
177
+ { Name: 'IssueID', Value: this.ID, Type: 'Input' },
178
+ { Name: 'IssueNumber', Value: this.IssueNumber, Type: 'Input' },
179
+ { Name: 'Title', Value: this.Title, Type: 'Input' },
180
+ { Name: 'StatusID', Value: this.StatusID, Type: 'Input' },
181
+ { Name: 'Priority', Value: this.Priority, Type: 'Input' },
182
+ { Name: 'Severity', Value: this.Severity, Type: 'Input' },
183
+ ];
184
+ await ActionEngineServer.Instance.RunAction({
185
+ Action: action,
186
+ ContextUser: contextUser,
187
+ Params: params,
188
+ Filters: [],
189
+ });
190
+ }
191
+ catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ LogError(`IssueEntityServer.fireHook: failed to run Action ${actionID}: ${msg}`);
194
+ }
195
+ }
196
+ };
197
+ IssueEntityServer = __decorate([
198
+ RegisterClass(BaseEntity, 'MJ_BizApps_Issues: Issues', 2)
199
+ ], IssueEntityServer);
200
+ export { IssueEntityServer };
201
+ //# sourceMappingURL=IssueEntityServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IssueEntityServer.js","sourceRoot":"","sources":["../src/IssueEntityServer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { BaseEntity } from '@memberjunction/core';
2
+ /**
3
+ * SequenceService — calls the DB-level atomic numbering routine (spAssignNextIssueNumber)
4
+ * from TypeScript so the IssueEntityServer hook can assign IssueNumber before
5
+ * super.Save() commits the row.
6
+ *
7
+ * The routine is intentionally kept at DB level because it requires atomic
8
+ * read-modify-write semantics that don't translate safely to app-level code
9
+ * under concurrency. It normalizes the scope (trim/UPPER, null/blank → 'ISS')
10
+ * and returns the formatted, unpadded `{SCOPE}-{seq}`.
11
+ *
12
+ * Platform-aware: MemberJunction runs on SQL Server or PostgreSQL, and the two
13
+ * expose the routine differently —
14
+ * * SQL Server: a PROCEDURE with an OUTPUT parameter, invoked via
15
+ * `DECLARE @x …; EXEC … @IssueNumber = @x OUTPUT; SELECT @x`.
16
+ * * PostgreSQL: a FUNCTION RETURNING TEXT (PG has no clean OUTPUT-param EXEC
17
+ * idiom), invoked via `SELECT schema.spAssignNextIssueNumber(:scope)`. The
18
+ * PG definition ships as a `.pg-only.sql` supplement under `migrations-pg/`.
19
+ * The dialect is selected from `DB_PLATFORM` (the same env var the MJ CLI and
20
+ * runtime use), defaulting to SQL Server when unset — matching the rest of the stack.
21
+ */
22
+ export declare class SequenceService {
23
+ /**
24
+ * Atomically allocates the next IssueNumber for the given AppScope and returns the
25
+ * formatted value (e.g. `MJC-42`, or `ISS-7` when AppScope is null/blank).
26
+ *
27
+ * The provider is taken from the calling entity so the call runs on the same
28
+ * (per-request) provider that owns the entity — never the process-global default.
29
+ *
30
+ * @param appScope The Issue's AppScope (may be null/blank — the routine defaults it to 'ISS').
31
+ * @param entity The Issue entity instance driving the save (supplies the provider + user).
32
+ */
33
+ static assignNextIssueNumber(appScope: string | null, entity: BaseEntity): Promise<string>;
34
+ /**
35
+ * SQL Server: call the OUTPUT-parameter procedure and surface its value as a column.
36
+ * The proc re-normalizes the scope regardless of what we pass; we inline it as an escaped
37
+ * literal (the verified pattern across MJ's generated resolvers, which pass `undefined` params).
38
+ */
39
+ private static buildSqlServerSQL;
40
+ /**
41
+ * PostgreSQL: call the function form and alias its scalar result to the `IssueNumber` column the
42
+ * caller reads. The function name is emitted UNQUOTED so PostgreSQL folds it to the same lowercase
43
+ * identifier the `CREATE FUNCTION` produced. The schema is likewise unquoted (folds to lowercase),
44
+ * matching how `mj codegen` and the MJServer runtime reference app-schema objects on PG.
45
+ */
46
+ private static buildPostgresSQL;
47
+ }
48
+ //# sourceMappingURL=SequenceService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SequenceService.d.ts","sourceRoot":"","sources":["../src/SequenceService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkC,MAAM,sBAAsB,CAAC;AAKlF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,eAAe;IAC1B;;;;;;;;;OASG;WACiB,qBAAqB,CACvC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,MAAM,CAAC;IAkClB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAWhC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;CAIhC"}
@@ -0,0 +1,83 @@
1
+ import { LogError } from '@memberjunction/core';
2
+ import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
3
+ const ISSUES_SCHEMA = '__mj_BizAppsIssues';
4
+ /**
5
+ * SequenceService — calls the DB-level atomic numbering routine (spAssignNextIssueNumber)
6
+ * from TypeScript so the IssueEntityServer hook can assign IssueNumber before
7
+ * super.Save() commits the row.
8
+ *
9
+ * The routine is intentionally kept at DB level because it requires atomic
10
+ * read-modify-write semantics that don't translate safely to app-level code
11
+ * under concurrency. It normalizes the scope (trim/UPPER, null/blank → 'ISS')
12
+ * and returns the formatted, unpadded `{SCOPE}-{seq}`.
13
+ *
14
+ * Platform-aware: MemberJunction runs on SQL Server or PostgreSQL, and the two
15
+ * expose the routine differently —
16
+ * * SQL Server: a PROCEDURE with an OUTPUT parameter, invoked via
17
+ * `DECLARE @x …; EXEC … @IssueNumber = @x OUTPUT; SELECT @x`.
18
+ * * PostgreSQL: a FUNCTION RETURNING TEXT (PG has no clean OUTPUT-param EXEC
19
+ * idiom), invoked via `SELECT schema.spAssignNextIssueNumber(:scope)`. The
20
+ * PG definition ships as a `.pg-only.sql` supplement under `migrations-pg/`.
21
+ * The dialect is selected from `DB_PLATFORM` (the same env var the MJ CLI and
22
+ * runtime use), defaulting to SQL Server when unset — matching the rest of the stack.
23
+ */
24
+ export class SequenceService {
25
+ /**
26
+ * Atomically allocates the next IssueNumber for the given AppScope and returns the
27
+ * formatted value (e.g. `MJC-42`, or `ISS-7` when AppScope is null/blank).
28
+ *
29
+ * The provider is taken from the calling entity so the call runs on the same
30
+ * (per-request) provider that owns the entity — never the process-global default.
31
+ *
32
+ * @param appScope The Issue's AppScope (may be null/blank — the routine defaults it to 'ISS').
33
+ * @param entity The Issue entity instance driving the save (supplies the provider + user).
34
+ */
35
+ static async assignNextIssueNumber(appScope, entity) {
36
+ // ProviderToUse is typed IEntityDataProvider, but every concrete MJ provider (SQL Server /
37
+ // PostgreSQL) is a DatabaseProviderBase subclass that also implements IEntityDataProvider. The two
38
+ // interfaces don't structurally overlap, so TS requires the widening cast through `unknown` — this
39
+ // is the sanctioned mechanism for a cast between compatible-at-runtime types, NOT an `any` escape.
40
+ const provider = entity.ProviderToUse;
41
+ if (!provider) {
42
+ LogError('SequenceService.assignNextIssueNumber: entity has no provider');
43
+ throw new Error('SequenceService: no provider available on the entity');
44
+ }
45
+ // DB_PLATFORM defaults to SQL Server when unset (resolveDbPlatformFromEnv returns undefined).
46
+ const platform = resolveDbPlatformFromEnv() ?? 'sqlserver';
47
+ const sql = platform === 'postgresql'
48
+ ? this.buildPostgresSQL(appScope)
49
+ : this.buildSqlServerSQL(appScope);
50
+ const rows = await provider.ExecuteSQL(sql, undefined, { isMutation: true, description: 'spAssignNextIssueNumber' }, entity.ContextCurrentUser);
51
+ const value = rows?.[0]?.IssueNumber;
52
+ if (!value || typeof value !== 'string') {
53
+ throw new Error(`SequenceService.assignNextIssueNumber: routine returned no value for AppScope=${appScope ?? '(null)'}`);
54
+ }
55
+ return value;
56
+ }
57
+ /**
58
+ * SQL Server: call the OUTPUT-parameter procedure and surface its value as a column.
59
+ * The proc re-normalizes the scope regardless of what we pass; we inline it as an escaped
60
+ * literal (the verified pattern across MJ's generated resolvers, which pass `undefined` params).
61
+ */
62
+ static buildSqlServerSQL(appScope) {
63
+ const scopeLiteral = appScope == null ? 'NULL' : `N'${appScope.replace(/'/g, "''")}'`;
64
+ return `
65
+ DECLARE @issueNumber NVARCHAR(50);
66
+ EXEC ${ISSUES_SCHEMA}.spAssignNextIssueNumber
67
+ @AppScope = ${scopeLiteral},
68
+ @IssueNumber = @issueNumber OUTPUT;
69
+ SELECT @issueNumber AS IssueNumber;
70
+ `;
71
+ }
72
+ /**
73
+ * PostgreSQL: call the function form and alias its scalar result to the `IssueNumber` column the
74
+ * caller reads. The function name is emitted UNQUOTED so PostgreSQL folds it to the same lowercase
75
+ * identifier the `CREATE FUNCTION` produced. The schema is likewise unquoted (folds to lowercase),
76
+ * matching how `mj codegen` and the MJServer runtime reference app-schema objects on PG.
77
+ */
78
+ static buildPostgresSQL(appScope) {
79
+ const scopeLiteral = appScope == null ? 'NULL' : `'${appScope.replace(/'/g, "''")}'`;
80
+ return `SELECT ${ISSUES_SCHEMA}.spAssignNextIssueNumber(${scopeLiteral}) AS "IssueNumber";`;
81
+ }
82
+ }
83
+ //# sourceMappingURL=SequenceService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SequenceService.js","sourceRoot":"","sources":["../src/SequenceService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AAErF,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,eAAe;IAC1B;;;;;;;;;OASG;IACI,MAAM,CAAC,KAAK,CAAC,qBAAqB,CACvC,QAAuB,EACvB,MAAkB;QAElB,2FAA2F;QAC3F,mGAAmG;QACnG,mGAAmG;QACnG,mGAAmG;QACnG,MAAM,QAAQ,GAAG,MAAM,CAAC,aAA4D,CAAC;QACrF,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,QAAQ,CAAC,+DAA+D,CAAC,CAAC;YAC1E,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QAED,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,wBAAwB,EAAE,IAAI,WAAW,CAAC;QAC3D,MAAM,GAAG,GACP,QAAQ,KAAK,YAAY;YACvB,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC;YACjC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAEvC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,UAAU,CACpC,GAAG,EACH,SAAS,EACT,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,yBAAyB,EAAE,EAC5D,MAAM,CAAC,kBAAkB,CAC1B,CAAC;QAEF,MAAM,KAAK,GAAI,IAAI,EAAE,CAAC,CAAC,CAA2C,EAAE,WAAW,CAAC;QAChF,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CACb,iFAAiF,QAAQ,IAAI,QAAQ,EAAE,CACxG,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,iBAAiB,CAAC,QAAuB;QACtD,MAAM,YAAY,GAAG,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;QACtF,OAAO;;aAEE,aAAa;wBACF,YAAY;;;KAG/B,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACK,MAAM,CAAC,gBAAgB,CAAC,QAAuB;QACrD,MAAM,YAAY,GAAG,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;QACrF,OAAO,UAAU,aAAa,4BAA4B,YAAY,qBAAqB,CAAC;IAC9F,CAAC;CACF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @mj-biz-apps/issues-core-entities-server
3
+ *
4
+ * Server-side entity subclasses for BizApps Issues entities. These hold side-effects
5
+ * that must run server-only and authoritatively (not in the browser) — currently the
6
+ * assignment of the human-readable Issue.IssueNumber via an atomic DB sequence proc.
7
+ *
8
+ * Import this package (or call LoadBizAppsIssuesEntitiesServer) from the server
9
+ * bootstrap so the @RegisterClass decorators fire at startup.
10
+ */
11
+ export { IssueEntityServer } from './IssueEntityServer.js';
12
+ export { SequenceService } from './SequenceService.js';
13
+ /**
14
+ * Forces the server-side entity subclasses to load so their @RegisterClass
15
+ * decorators register (priority 2, overriding the client-shared entities).
16
+ * Tree-shaking would otherwise drop them since they're referenced only via the
17
+ * class factory.
18
+ */
19
+ export declare function LoadBizAppsIssuesEntitiesServer(): void;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,+BAA+B,IAAI,IAAI,CAGtD"}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @mj-biz-apps/issues-core-entities-server
3
+ *
4
+ * Server-side entity subclasses for BizApps Issues entities. These hold side-effects
5
+ * that must run server-only and authoritatively (not in the browser) — currently the
6
+ * assignment of the human-readable Issue.IssueNumber via an atomic DB sequence proc.
7
+ *
8
+ * Import this package (or call LoadBizAppsIssuesEntitiesServer) from the server
9
+ * bootstrap so the @RegisterClass decorators fire at startup.
10
+ */
11
+ import { IssueEntityServer } from './IssueEntityServer.js';
12
+ export { IssueEntityServer } from './IssueEntityServer.js';
13
+ export { SequenceService } from './SequenceService.js';
14
+ /**
15
+ * Forces the server-side entity subclasses to load so their @RegisterClass
16
+ * decorators register (priority 2, overriding the client-shared entities).
17
+ * Tree-shaking would otherwise drop them since they're referenced only via the
18
+ * class factory.
19
+ */
20
+ export function LoadBizAppsIssuesEntitiesServer() {
21
+ // Reference the class so the import is retained and its decorator runs.
22
+ void IssueEntityServer;
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,UAAU,+BAA+B;IAC7C,wEAAwE;IACxE,KAAK,iBAAiB,CAAC;AACzB,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,40 @@
1
1
  {
2
2
  "name": "@mj-biz-apps/issues-core-entities-server",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @mj-biz-apps/issues-core-entities-server",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Server-side entity subclasses for BizApps Issues — lifecycle hooks for Issue, IssueComment, etc. (stamp ResolvedAt/ClosedAt, fire IssueType action hooks).",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "/dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && tsc-alias -f",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
18
+ },
19
+ "author": "MemberJunction.com",
20
+ "license": "ISC",
21
+ "devDependencies": {
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "dependencies": {
25
+ "@mj-biz-apps/issues-entities": "1.0.0",
26
+ "@mj-biz-apps/issues-core": "1.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@memberjunction/core": "^5.40.2",
30
+ "@memberjunction/core-entities": "^5.40.2",
31
+ "@memberjunction/generic-database-provider": "^5.40.2",
32
+ "@memberjunction/global": "^5.40.2",
33
+ "@memberjunction/actions": "^5.40.2",
34
+ "@memberjunction/actions-base": "^5.40.2"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/MemberJunction/bizapps-issues"
39
+ }
10
40
  }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @mj-biz-apps/issues-core-entities-server
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@mj-biz-apps/issues-core-entities-server`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**