@naisys/erp 3.0.0-beta.8 → 3.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.
Files changed (83) hide show
  1. package/.env.example +5 -0
  2. package/client-dist/assets/index-CSiMTJfw.css +14 -0
  3. package/client-dist/assets/index-D6lSIioV.js +11167 -0
  4. package/client-dist/assets/rolldown-runtime-CvHMtSRF.js +33 -0
  5. package/client-dist/assets/vendor-CJ0ET9hP.js +75181 -0
  6. package/client-dist/assets/vendor-CLUPjUnv.css +8747 -0
  7. package/client-dist/favicon-16x16.png +0 -0
  8. package/client-dist/favicon-32x32.png +0 -0
  9. package/client-dist/favicon.ico +0 -0
  10. package/client-dist/index.html +17 -2
  11. package/dist/{dbConfig.js → database/dbConfig.js} +1 -1
  12. package/dist/{erpDb.js → database/erpDb.js} +1 -1
  13. package/dist/erpRoutes.js +115 -0
  14. package/dist/erpServer.js +85 -161
  15. package/dist/error-handler.js +3 -0
  16. package/dist/generated/prisma/internal/class.js +4 -4
  17. package/dist/generated/prisma/internal/prismaNamespace.js +3 -1
  18. package/dist/middleware/auth-middleware.js +146 -0
  19. package/dist/route-helpers.js +2 -2
  20. package/dist/routes/admin.js +15 -7
  21. package/dist/routes/audit.js +1 -1
  22. package/dist/routes/{item-fields.js → items/item-fields.js} +24 -22
  23. package/dist/routes/{item-instances.js → items/item-instances.js} +42 -24
  24. package/dist/routes/{items.js → items/items.js} +35 -33
  25. package/dist/routes/{operation-dependencies.js → operations/operation-dependencies.js} +6 -6
  26. package/dist/routes/{operation-field-refs.js → operations/operation-field-refs.js} +6 -6
  27. package/dist/routes/{operation-run-comments.js → operations/operation-run-comments.js} +5 -5
  28. package/dist/routes/{operation-run-transitions.js → operations/operation-run-transitions.js} +29 -13
  29. package/dist/routes/{operation-runs.js → operations/operation-runs.js} +48 -10
  30. package/dist/routes/{operations.js → operations/operations.js} +6 -6
  31. package/dist/routes/{order-revision-transitions.js → orders/order-revision-transitions.js} +4 -4
  32. package/dist/routes/{order-revisions.js → orders/order-revisions.js} +6 -6
  33. package/dist/routes/{order-run-transitions.js → orders/order-run-transitions.js} +11 -5
  34. package/dist/routes/{order-runs.js → orders/order-runs.js} +7 -5
  35. package/dist/routes/{orders.js → orders/orders.js} +15 -11
  36. package/dist/routes/{dispatch.js → production/dispatch.js} +88 -7
  37. package/dist/routes/{inventory.js → production/inventory.js} +33 -10
  38. package/dist/routes/{labor-tickets.js → production/labor-tickets.js} +7 -7
  39. package/dist/routes/{work-centers.js → production/work-centers.js} +29 -29
  40. package/dist/routes/root.js +1 -1
  41. package/dist/routes/{step-field-attachments.js → steps/step-field-attachments.js} +8 -8
  42. package/dist/routes/{step-fields.js → steps/step-fields.js} +6 -6
  43. package/dist/routes/{step-run-fields.js → steps/step-run-fields.js} +9 -9
  44. package/dist/routes/{step-run-transitions.js → steps/step-run-transitions.js} +6 -6
  45. package/dist/routes/{step-runs.js → steps/step-runs.js} +7 -7
  46. package/dist/routes/{steps.js → steps/steps.js} +5 -5
  47. package/dist/routes/{auth.js → users/auth.js} +11 -23
  48. package/dist/routes/{user-permissions.js → users/user-permissions.js} +21 -7
  49. package/dist/routes/{users.js → users/users.js} +43 -21
  50. package/dist/services/attachment-service.js +2 -2
  51. package/dist/services/{item-instance-service.js → inventory/item-instance-service.js} +2 -2
  52. package/dist/services/{item-service.js → inventory/item-service.js} +2 -2
  53. package/dist/services/{operation-dependency-service.js → operations/operation-dependency-service.js} +1 -1
  54. package/dist/services/{operation-run-comment-service.js → operations/operation-run-comment-service.js} +1 -1
  55. package/dist/services/{operation-run-service.js → operations/operation-run-service.js} +15 -4
  56. package/dist/services/{operation-service.js → operations/operation-service.js} +2 -2
  57. package/dist/services/{step-run-service.js → operations/step-run-service.js} +1 -1
  58. package/dist/services/{step-service.js → operations/step-service.js} +2 -2
  59. package/dist/services/{order-revision-service.js → orders/order-revision-service.js} +4 -5
  60. package/dist/services/{order-run-service.js → orders/order-run-service.js} +68 -22
  61. package/dist/services/{order-service.js → orders/order-service.js} +11 -2
  62. package/dist/services/{revision-diff-service.js → orders/revision-diff-service.js} +11 -10
  63. package/dist/services/{field-ref-service.js → production/field-ref-service.js} +1 -1
  64. package/dist/services/{field-service.js → production/field-service.js} +2 -2
  65. package/dist/services/{field-value-service.js → production/field-value-service.js} +27 -3
  66. package/dist/services/production/labor-ticket-backfill.js +67 -0
  67. package/dist/services/{labor-ticket-service.js → production/labor-ticket-service.js} +21 -15
  68. package/dist/services/{work-center-service.js → production/work-center-service.js} +2 -2
  69. package/dist/services/user-service.js +94 -28
  70. package/dist/version.js +12 -0
  71. package/npm-shrinkwrap.json +3000 -0
  72. package/package.json +11 -9
  73. package/prisma/migrations/20260427010000_hash_user_api_keys/migration.sql +10 -0
  74. package/prisma/migrations/20260427020000_nullable_user_password_hash/migration.sql +39 -0
  75. package/prisma/migrations/20260517000000_add_op_run_tokens/migration.sql +2 -0
  76. package/prisma/schema.prisma +4 -2
  77. package/client-dist/assets/index-45dVo30p.css +0 -1
  78. package/client-dist/assets/index-C9uuPHLH.js +0 -168
  79. package/dist/auth-middleware.js +0 -203
  80. package/dist/userService.js +0 -118
  81. /package/bin/{naisys-erp → naisys-erp.js} +0 -0
  82. /package/dist/{supervisorAuth.js → middleware/supervisorAuth.js} +0 -0
  83. /package/dist/{audit.js → services/audit.js} +0 -0
@@ -225,6 +225,7 @@ export const OperationRunScalarFieldEnum = {
225
225
  status: 'status',
226
226
  assignedToId: 'assignedToId',
227
227
  cost: 'cost',
228
+ tokens: 'tokens',
228
229
  statusNote: 'statusNote',
229
230
  completedAt: 'completedAt',
230
231
  createdAt: 'createdAt',
@@ -282,7 +283,7 @@ export const UserScalarFieldEnum = {
282
283
  uuid: 'uuid',
283
284
  username: 'username',
284
285
  passwordHash: 'passwordHash',
285
- apiKey: 'apiKey',
286
+ apiKeyHash: 'apiKeyHash',
286
287
  isAgent: 'isAgent',
287
288
  createdAt: 'createdAt',
288
289
  updatedAt: 'updatedAt',
@@ -315,6 +316,7 @@ export const LaborTicketScalarFieldEnum = {
315
316
  clockIn: 'clockIn',
316
317
  clockOut: 'clockOut',
317
318
  cost: 'cost',
319
+ tokens: 'tokens',
318
320
  createdAt: 'createdAt',
319
321
  createdById: 'createdById',
320
322
  updatedAt: 'updatedAt',
@@ -0,0 +1,146 @@
1
+ import { AuthCache, urlMatchesPrefix } from "@naisys/common";
2
+ import { extractBearerToken, hashToken, SESSION_COOKIE_NAME, } from "@naisys/common-node";
3
+ import { findAgentByApiKey } from "@naisys/hub-database";
4
+ import { findSession, findUserByApiKey } from "@naisys/supervisor-database";
5
+ import erpDb from "../database/erpDb.js";
6
+ import { isSupervisorAuth } from "./supervisorAuth.js";
7
+ const PUBLIC_PREFIXES = ["/erp/api/auth/login", "/erp/api/client-config"];
8
+ export const authCache = new AuthCache();
9
+ async function loadPermissions(userId) {
10
+ const perms = await erpDb.userPermission.findMany({
11
+ where: { userId },
12
+ select: { permission: true },
13
+ });
14
+ return perms.map((p) => p.permission);
15
+ }
16
+ async function materializeErpUser(localUser) {
17
+ return {
18
+ id: localUser.id,
19
+ username: localUser.username,
20
+ permissions: await loadPermissions(localUser.id),
21
+ };
22
+ }
23
+ async function resolveCookie(token) {
24
+ const tokenHash = hashToken(token);
25
+ return authCache.getOrLoad(`cookie:${tokenHash}`, async () => {
26
+ const localUser = isSupervisorAuth()
27
+ ? await loadCookieUserSso(tokenHash)
28
+ : await loadCookieUserStandalone(tokenHash);
29
+ return localUser ? materializeErpUser(localUser) : null;
30
+ });
31
+ }
32
+ async function loadCookieUserSso(tokenHash) {
33
+ const session = await findSession(tokenHash);
34
+ if (!session)
35
+ return null;
36
+ return erpDb.user.upsert({
37
+ where: { uuid: session.uuid },
38
+ create: { uuid: session.uuid, username: session.username },
39
+ update: {},
40
+ });
41
+ }
42
+ async function loadCookieUserStandalone(tokenHash) {
43
+ const session = await erpDb.session.findUnique({
44
+ where: { tokenHash, expiresAt: { gt: new Date() } },
45
+ include: { user: true },
46
+ });
47
+ return session?.user ?? null;
48
+ }
49
+ async function resolveApiKey(apiKey) {
50
+ const apiKeyHash = hashToken(apiKey);
51
+ return authCache.getOrLoad(`apikey:${apiKeyHash}`, async () => {
52
+ const localUser = isSupervisorAuth()
53
+ ? await loadApiKeyUserSso(apiKey)
54
+ : await erpDb.user.findUnique({ where: { apiKeyHash } });
55
+ return localUser ? materializeErpUser(localUser) : null;
56
+ });
57
+ }
58
+ async function loadApiKeyUserSso(apiKey) {
59
+ // Try supervisor DB (humans + agents with external keys),
60
+ // then hub DB (agents matching their hub-issued runtime key).
61
+ const supervisorUser = await findUserByApiKey(apiKey);
62
+ const hubAgent = supervisorUser ? null : await findAgentByApiKey(apiKey);
63
+ const match = supervisorUser ?? hubAgent;
64
+ if (!match)
65
+ return null;
66
+ const isAgent = supervisorUser?.isAgent ?? !!hubAgent;
67
+ return erpDb.user.upsert({
68
+ where: { uuid: match.uuid },
69
+ create: { uuid: match.uuid, username: match.username, isAgent },
70
+ update: {},
71
+ });
72
+ }
73
+ export function hasPermission(user, permission) {
74
+ if (!user)
75
+ return false;
76
+ return (user.permissions.includes(permission) ||
77
+ user.permissions.includes("erp_admin"));
78
+ }
79
+ export function requirePermission(permission) {
80
+ return async (request, reply) => {
81
+ if (!request.erpUser) {
82
+ reply.status(401).send({
83
+ statusCode: 401,
84
+ error: "Unauthorized",
85
+ message: "Authentication required",
86
+ });
87
+ return;
88
+ }
89
+ if (!hasPermission(request.erpUser, permission)) {
90
+ reply.status(403).send({
91
+ statusCode: 403,
92
+ error: "Forbidden",
93
+ message: `Permission '${permission}' required`,
94
+ missingPermission: permission,
95
+ });
96
+ return;
97
+ }
98
+ };
99
+ }
100
+ function isPublicRoute(url) {
101
+ // Exact match: API root
102
+ if (url === "/erp/api/" || url === "/erp/api")
103
+ return true;
104
+ for (const prefix of PUBLIC_PREFIXES) {
105
+ if (urlMatchesPrefix(url, prefix))
106
+ return true;
107
+ }
108
+ if (urlMatchesPrefix(url, "/erp/api/schemas"))
109
+ return true;
110
+ // Non-ERP-API paths (static files, supervisor routes, etc.)
111
+ if (!url.startsWith("/erp/api"))
112
+ return true;
113
+ return false;
114
+ }
115
+ export function registerAuthMiddleware(fastify) {
116
+ const publicRead = process.env.PUBLIC_READ === "true";
117
+ fastify.decorateRequest("erpUser", undefined);
118
+ fastify.addHook("onRequest", async (request, reply) => {
119
+ const token = request.cookies?.[SESSION_COOKIE_NAME];
120
+ if (token) {
121
+ const user = await resolveCookie(token);
122
+ if (user)
123
+ request.erpUser = user;
124
+ }
125
+ if (!request.erpUser) {
126
+ const apiKey = extractBearerToken(request.headers.authorization);
127
+ if (apiKey) {
128
+ const user = await resolveApiKey(apiKey);
129
+ if (user)
130
+ request.erpUser = user;
131
+ }
132
+ }
133
+ if (request.erpUser)
134
+ return; // Authenticated, always allowed
135
+ if (isPublicRoute(request.url))
136
+ return; // Public route
137
+ if (publicRead && request.method === "GET")
138
+ return; // Public read mode
139
+ reply.status(401).send({
140
+ statusCode: 401,
141
+ error: "Unauthorized",
142
+ message: "Authentication required",
143
+ });
144
+ });
145
+ }
146
+ //# sourceMappingURL=auth-middleware.js.map
@@ -1,8 +1,8 @@
1
1
  import { permGate, resolveActions as resolveActionsBase, } from "@naisys/common";
2
2
  import { OperationRunStatus, OrderRunStatus, RevisionStatus, } from "@naisys/erp-shared";
3
- import { hasPermission } from "./auth-middleware.js";
4
- import erpDb from "./erpDb.js";
3
+ import erpDb from "./database/erpDb.js";
5
4
  import { API_PREFIX, schemaLink, selfLink } from "./hateoas.js";
5
+ import { hasPermission } from "./middleware/auth-middleware.js";
6
6
  // --- Prefer: return=representation (RFC 7240) ---
7
7
  /**
8
8
  * Returns true when the caller wants the full entity in the mutation response.
@@ -1,13 +1,15 @@
1
1
  import { createReadStream, existsSync, statSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { AdminAttachmentListRequestSchema, AdminAttachmentListResponseSchema, AdminInfoResponseSchema, ErrorResponseSchema, ServerLogRequestSchema, ServerLogResponseSchema, } from "@naisys/erp-shared";
4
+ import { getHubVariable } from "@naisys/hub-database";
4
5
  import { z } from "zod/v4";
5
- import { hasPermission, requirePermission } from "../auth-middleware.js";
6
- import { erpDbPath } from "../dbConfig.js";
7
- import erpDb from "../erpDb.js";
6
+ import { ERP_DB_VERSION, erpDbPath } from "../database/dbConfig.js";
7
+ import erpDb from "../database/erpDb.js";
8
8
  import { notFound } from "../error-handler.js";
9
9
  import { paginationLinks } from "../hateoas.js";
10
+ import { hasPermission, requirePermission, } from "../middleware/auth-middleware.js";
10
11
  import { getErpLogPath, tailLogFile } from "../services/log-file-service.js";
12
+ import { getPackageVersion } from "../version.js";
11
13
  const API_PREFIX = "/erp/api";
12
14
  function adminActions(hasAdminPermission) {
13
15
  const actions = [];
@@ -44,13 +46,19 @@ export default function adminRoutes(fastify, _options) {
44
46
  const hasAdminPerm = hasPermission(request.erpUser, "erp_admin");
45
47
  const actions = adminActions(hasAdminPerm);
46
48
  const dbPath = erpDbPath();
47
- const erpDbSize = await fs
48
- .stat(dbPath)
49
- .then((s) => s.size)
50
- .catch(() => undefined);
49
+ const [erpDbSize, targetVersion] = await Promise.all([
50
+ fs
51
+ .stat(dbPath)
52
+ .then((s) => s.size)
53
+ .catch(() => undefined),
54
+ getHubVariable("TARGET_VERSION"),
55
+ ]);
51
56
  return {
57
+ erpVersion: getPackageVersion(),
52
58
  erpDbPath: dbPath,
53
59
  erpDbSize,
60
+ erpDbVersion: ERP_DB_VERSION,
61
+ targetVersion: targetVersion || undefined,
54
62
  _actions: actions.length > 0 ? actions : undefined,
55
63
  };
56
64
  });
@@ -1,5 +1,5 @@
1
1
  import { AuditListResponseSchema, AuditQuerySchema } from "@naisys/erp-shared";
2
- import erpDb from "../erpDb.js";
2
+ import erpDb from "../database/erpDb.js";
3
3
  export default function auditRoutes(fastify) {
4
4
  const app = fastify.withTypeProvider();
5
5
  app.get("/", {
@@ -1,12 +1,12 @@
1
1
  import { CreateFieldSchema, ErrorResponseSchema, FieldListResponseSchema, FieldSchema, MutateResponseSchema, SeqNoCreateResponseSchema, UpdateFieldSchema, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { hasPermission, requirePermission } from "../auth-middleware.js";
4
- import erpDb from "../erpDb.js";
5
- import { notFound } from "../error-handler.js";
6
- import { API_PREFIX, selfLink } from "../hateoas.js";
7
- import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, } from "../route-helpers.js";
8
- import { createField, deleteField, ensureFieldSet, findExistingField, getField, listFields, updateField, } from "../services/field-service.js";
9
- import { findExisting as findExistingItem } from "../services/item-service.js";
3
+ import erpDb from "../../database/erpDb.js";
4
+ import { notFound } from "../../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
6
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
7
+ import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, permGate, } from "../../route-helpers.js";
8
+ import { findExisting as findExistingItem } from "../../services/inventory/item-service.js";
9
+ import { createField, deleteField, ensureFieldSet, findExistingField, getField, listFields, updateField, } from "../../services/production/field-service.js";
10
10
  const ParamsSchema = z.object({ key: z.string() });
11
11
  const FieldParamsSchema = z.object({
12
12
  key: z.string(),
@@ -27,23 +27,26 @@ function formatField(key, user, field) {
27
27
  required: field.required,
28
28
  ...formatAuditFields(field),
29
29
  _links: childItemLinks(base, field.seqNo, "Fields", `/items/${key}`, "Item", "Field"),
30
- _actions: hasPermission(user, "item_manager")
31
- ? [
30
+ _actions: (() => {
31
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
32
+ return [
32
33
  {
33
34
  rel: "update",
34
35
  href: `${API_PREFIX}${base}/${field.seqNo}`,
35
36
  method: "PUT",
36
37
  title: "Update",
37
38
  schema: `${API_PREFIX}/schemas/UpdateField`,
39
+ ...gate,
38
40
  },
39
41
  {
40
42
  rel: "delete",
41
43
  href: `${API_PREFIX}${base}/${field.seqNo}`,
42
44
  method: "DELETE",
43
45
  title: "Delete",
46
+ ...gate,
44
47
  },
45
- ]
46
- : [],
48
+ ];
49
+ })(),
47
50
  };
48
51
  }
49
52
  export default function itemFieldRoutes(fastify) {
@@ -81,17 +84,16 @@ export default function itemFieldRoutes(fastify) {
81
84
  hrefTemplate: `${API_PREFIX}${base}/{seqNo}`,
82
85
  },
83
86
  ],
84
- _actions: hasPermission(request.erpUser, "item_manager")
85
- ? [
86
- {
87
- rel: "create",
88
- href: `${API_PREFIX}${base}`,
89
- method: "POST",
90
- title: "Add Field",
91
- schema: `${API_PREFIX}/schemas/CreateField`,
92
- },
93
- ]
94
- : [],
87
+ _actions: [
88
+ {
89
+ rel: "create",
90
+ href: `${API_PREFIX}${base}`,
91
+ method: "POST",
92
+ title: "Add Field",
93
+ schema: `${API_PREFIX}/schemas/CreateField`,
94
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
95
+ },
96
+ ],
95
97
  };
96
98
  },
97
99
  });
@@ -1,12 +1,12 @@
1
1
  import { CreateItemInstanceSchema, DeleteSetMutateResponseSchema, ErrorResponseSchema, fieldTypeString, FieldValueMutateResponseSchema, getValueFormatHint, ItemInstanceListQuerySchema, ItemInstanceListResponseSchema, ItemInstanceSchema, KeyCreateResponseSchema, MutateResponseSchema, UpdateFieldValueSchema, UpdateItemInstanceSchema, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { hasPermission, requirePermission } from "../auth-middleware.js";
4
- import { notFound, unprocessable } from "../error-handler.js";
5
- import { API_PREFIX, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { formatAuditFields, mutationResult, useFullSerializer, wantsFullResponse, } from "../route-helpers.js";
7
- import { checkFieldValueShape, deleteFieldValueSet, deserializeFieldValue, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../services/field-value-service.js";
8
- import { createItemInstance, deleteItemInstance, ensureItemInstanceFieldRecord, findItemInstance, findItemInstanceWithField, listItemInstances, updateItemInstance, } from "../services/item-instance-service.js";
9
- import { findExisting as findItem } from "../services/item-service.js";
3
+ import { notFound, unprocessable } from "../../error-handler.js";
4
+ import { API_PREFIX, paginationLinks, schemaLink, selfLink, } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { formatAuditFields, mutationResult, permGate, useFullSerializer, wantsFullResponse, } from "../../route-helpers.js";
7
+ import { createItemInstance, deleteItemInstance, ensureItemInstanceFieldRecord, findItemInstance, findItemInstanceWithField, listItemInstances, updateItemInstance, } from "../../services/inventory/item-instance-service.js";
8
+ import { findExisting as findItem } from "../../services/inventory/item-service.js";
9
+ import { checkFieldValueShape, deleteFieldValueSet, deserializeFieldValue, serializeFieldValue, upsertFieldValue, validateFieldValue, } from "../../services/production/field-value-service.js";
10
10
  const ParamsSchema = z.object({
11
11
  key: z.string(),
12
12
  });
@@ -59,9 +59,8 @@ function instanceLinks(itemKey, instanceId, inst) {
59
59
  return links;
60
60
  }
61
61
  function instanceActions(itemKey, instanceId, user) {
62
- if (!hasPermission(user, "item_manager"))
63
- return [];
64
62
  const href = `${API_PREFIX}/${instanceBasePath(itemKey)}/${instanceId}`;
63
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
65
64
  return [
66
65
  {
67
66
  rel: "update",
@@ -70,12 +69,14 @@ function instanceActions(itemKey, instanceId, user) {
70
69
  title: "Update",
71
70
  schema: `${API_PREFIX}/schemas/UpdateItemInstance`,
72
71
  body: { key: "" },
72
+ ...gate,
73
73
  },
74
74
  {
75
75
  rel: "delete",
76
76
  href,
77
77
  method: "DELETE",
78
78
  title: "Delete",
79
+ ...gate,
79
80
  },
80
81
  ];
81
82
  }
@@ -122,17 +123,35 @@ function buildFieldValues(inst) {
122
123
  return fieldValues;
123
124
  }
124
125
  function buildActionTemplates(itemKey, instanceId, user, hasFields) {
125
- if (!hasPermission(user, "item_manager") || !hasFields)
126
+ if (!hasFields)
126
127
  return [];
127
128
  const instanceHref = `${API_PREFIX}/${instanceBasePath(itemKey)}/${instanceId}`;
129
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
128
130
  return [
129
131
  {
130
- rel: "updateField",
132
+ rel: "update-field-value",
131
133
  hrefTemplate: `${instanceHref}/fields/{fieldSeqNo}`,
132
134
  method: "PUT",
133
- title: "Update Field Value",
135
+ title: "Update Field Value (implicit set 0)",
136
+ schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
137
+ body: { value: "" },
138
+ ...gate,
139
+ },
140
+ {
141
+ rel: "update-set-field-value",
142
+ hrefTemplate: `${instanceHref}/sets/{setIndex}/fields/{fieldSeqNo}`,
143
+ method: "PUT",
144
+ title: "Update Field Value (explicit set index)",
134
145
  schema: `${API_PREFIX}/schemas/UpdateFieldValue`,
135
146
  body: { value: "" },
147
+ ...gate,
148
+ },
149
+ {
150
+ rel: "delete-set",
151
+ hrefTemplate: `${instanceHref}/sets/{setIndex}`,
152
+ method: "DELETE",
153
+ title: "Delete Field Value Set",
154
+ ...gate,
136
155
  },
137
156
  ];
138
157
  }
@@ -202,18 +221,17 @@ export default function itemInstanceRoutes(fastify) {
202
221
  hrefTemplate: `${API_PREFIX}/items/${key}/instances/{id}`,
203
222
  },
204
223
  ],
205
- _actions: hasPermission(request.erpUser, "item_manager")
206
- ? [
207
- {
208
- rel: "create",
209
- href: `${API_PREFIX}/${base}`,
210
- method: "POST",
211
- title: "Create Instance",
212
- schema: `${API_PREFIX}/schemas/CreateItemInstance`,
213
- body: { key: "" },
214
- },
215
- ]
216
- : [],
224
+ _actions: [
225
+ {
226
+ rel: "create",
227
+ href: `${API_PREFIX}/${base}`,
228
+ method: "POST",
229
+ title: "Create Instance",
230
+ schema: `${API_PREFIX}/schemas/CreateItemInstance`,
231
+ body: { key: "" },
232
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
233
+ },
234
+ ],
217
235
  };
218
236
  },
219
237
  });
@@ -1,10 +1,10 @@
1
1
  import { CreateItemSchema, ErrorResponseSchema, ItemListQuerySchema, ItemListResponseSchema, ItemSchema, KeyCreateResponseSchema, MutateResponseSchema, UpdateItemSchema, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { hasPermission, requirePermission } from "../auth-middleware.js";
4
- import { notFound } from "../error-handler.js";
5
- import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../hateoas.js";
6
- import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, } from "../route-helpers.js";
7
- import { createItem, deleteItem, findExisting, listItems, updateItem, } from "../services/item-service.js";
3
+ import { notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, collectionLink, paginationLinks, schemaLink, selfLink, } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { calcNextSeqNo, childItemLinks, formatAuditFields, mutationResult, permGate, } from "../../route-helpers.js";
7
+ import { createItem, deleteItem, findExisting, listItems, updateItem, } from "../../services/inventory/item-service.js";
8
8
  const RESOURCE = "items";
9
9
  const KeyParamsSchema = z.object({
10
10
  key: z.string(),
@@ -17,9 +17,8 @@ function itemLinks(key) {
17
17
  ];
18
18
  }
19
19
  function itemActions(key, user) {
20
- if (!hasPermission(user, "item_manager"))
21
- return [];
22
20
  const href = `${API_PREFIX}/${RESOURCE}/${key}`;
21
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
23
22
  return [
24
23
  {
25
24
  rel: "update",
@@ -27,12 +26,14 @@ function itemActions(key, user) {
27
26
  method: "PUT",
28
27
  title: "Update",
29
28
  schema: `${API_PREFIX}/schemas/UpdateItem`,
29
+ ...gate,
30
30
  },
31
31
  {
32
32
  rel: "delete",
33
33
  href,
34
34
  method: "DELETE",
35
35
  title: "Delete",
36
+ ...gate,
36
37
  },
37
38
  ];
38
39
  }
@@ -44,17 +45,16 @@ function formatItemFieldListResponse(itemKey, user, fields) {
44
45
  total: fields.length,
45
46
  nextSeqNo: calcNextSeqNo(maxSeq),
46
47
  _links: [selfLink(base)],
47
- _actions: hasPermission(user, "item_manager")
48
- ? [
49
- {
50
- rel: "create",
51
- href: `${API_PREFIX}${base}`,
52
- method: "POST",
53
- title: "Add Field",
54
- schema: `${API_PREFIX}/schemas/CreateField`,
55
- },
56
- ]
57
- : [],
48
+ _actions: [
49
+ {
50
+ rel: "create",
51
+ href: `${API_PREFIX}${base}`,
52
+ method: "POST",
53
+ title: "Add Field",
54
+ schema: `${API_PREFIX}/schemas/CreateField`,
55
+ ...permGate(hasPermission(user, "item_manager"), "item_manager"),
56
+ },
57
+ ],
58
58
  };
59
59
  }
60
60
  function formatItemField(itemKey, user, field) {
@@ -69,23 +69,26 @@ function formatItemField(itemKey, user, field) {
69
69
  required: field.required,
70
70
  ...formatAuditFields(field),
71
71
  _links: childItemLinks(base, field.seqNo, "Fields", `/items/${itemKey}`, "Item", "Field"),
72
- _actions: hasPermission(user, "item_manager")
73
- ? [
72
+ _actions: (() => {
73
+ const gate = permGate(hasPermission(user, "item_manager"), "item_manager");
74
+ return [
74
75
  {
75
76
  rel: "update",
76
77
  href: `${API_PREFIX}${base}/${field.seqNo}`,
77
78
  method: "PUT",
78
79
  title: "Update",
79
80
  schema: `${API_PREFIX}/schemas/UpdateField`,
81
+ ...gate,
80
82
  },
81
83
  {
82
84
  rel: "delete",
83
85
  href: `${API_PREFIX}${base}/${field.seqNo}`,
84
86
  method: "DELETE",
85
87
  title: "Delete",
88
+ ...gate,
86
89
  },
87
- ]
88
- : [],
90
+ ];
91
+ })(),
89
92
  };
90
93
  }
91
94
  function formatItem(item, user) {
@@ -139,17 +142,16 @@ export default function itemRoutes(fastify) {
139
142
  _linkTemplates: [
140
143
  { rel: "item", hrefTemplate: `${API_PREFIX}/items/{key}` },
141
144
  ],
142
- _actions: hasPermission(request.erpUser, "item_manager")
143
- ? [
144
- {
145
- rel: "create",
146
- href: `${API_PREFIX}/${RESOURCE}`,
147
- method: "POST",
148
- title: "Create Item",
149
- schema: `${API_PREFIX}/schemas/CreateItem`,
150
- },
151
- ]
152
- : [],
145
+ _actions: [
146
+ {
147
+ rel: "create",
148
+ href: `${API_PREFIX}/${RESOURCE}`,
149
+ method: "POST",
150
+ title: "Create Item",
151
+ schema: `${API_PREFIX}/schemas/CreateItem`,
152
+ ...permGate(hasPermission(request.erpUser, "item_manager"), "item_manager"),
153
+ },
154
+ ],
153
155
  };
154
156
  },
155
157
  });
@@ -1,11 +1,11 @@
1
1
  import { CreateOperationDependencySchema, CreateResponseSchema, ErrorResponseSchema, OperationDependencyListResponseSchema, RevisionStatus, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { hasPermission, requirePermission } from "../auth-middleware.js";
4
- import { conflict, notFound } from "../error-handler.js";
5
- import { API_PREFIX, selfLink } from "../hateoas.js";
6
- import { mutationResult, resolveRevision } from "../route-helpers.js";
7
- import { createDependency, deleteDependency, listDependencies, } from "../services/operation-dependency-service.js";
8
- import { findExisting } from "../services/operation-service.js";
3
+ import { conflict, notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
5
+ import { hasPermission, requirePermission, } from "../../middleware/auth-middleware.js";
6
+ import { mutationResult, resolveRevision } from "../../route-helpers.js";
7
+ import { createDependency, deleteDependency, listDependencies, } from "../../services/operations/operation-dependency-service.js";
8
+ import { findExisting } from "../../services/operations/operation-service.js";
9
9
  const ParamsSchema = z.object({
10
10
  orderKey: z.string(),
11
11
  revNo: z.coerce.number().int(),
@@ -1,11 +1,11 @@
1
1
  import { CreateFieldRefSchema, ErrorResponseSchema, FieldRefListResponseSchema, RevisionStatus, SeqNoCreateResponseSchema, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { requirePermission } from "../auth-middleware.js";
4
- import erpDb from "../erpDb.js";
5
- import { conflict, notFound } from "../error-handler.js";
6
- import { API_PREFIX, selfLink } from "../hateoas.js";
7
- import { calcNextSeqNo, childItemLinks, mutationResult, resolveActions, resolveOperation, } from "../route-helpers.js";
8
- import { checkDuplicateSource, createFieldRef, deleteFieldRef, findExistingFieldRef, listFieldRefs, } from "../services/field-ref-service.js";
3
+ import erpDb from "../../database/erpDb.js";
4
+ import { conflict, notFound } from "../../error-handler.js";
5
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
6
+ import { requirePermission } from "../../middleware/auth-middleware.js";
7
+ import { calcNextSeqNo, childItemLinks, mutationResult, resolveActions, resolveOperation, } from "../../route-helpers.js";
8
+ import { checkDuplicateSource, createFieldRef, deleteFieldRef, findExistingFieldRef, listFieldRefs, } from "../../services/production/field-ref-service.js";
9
9
  const ParamsSchema = z.object({
10
10
  orderKey: z.string(),
11
11
  revNo: z.coerce.number().int(),
@@ -1,10 +1,10 @@
1
1
  import { CreateOperationRunCommentSchema, CreateResponseSchema, ErrorResponseSchema, OperationRunCommentListResponseSchema, OperationRunCommentType, } from "@naisys/erp-shared";
2
2
  import { z } from "zod/v4";
3
- import { requirePermission } from "../auth-middleware.js";
4
- import { notFound } from "../error-handler.js";
5
- import { API_PREFIX, selfLink } from "../hateoas.js";
6
- import { mutationResult, resolveActions, resolveOpRun, } from "../route-helpers.js";
7
- import { createComment, listComments, } from "../services/operation-run-comment-service.js";
3
+ import { notFound } from "../../error-handler.js";
4
+ import { API_PREFIX, selfLink } from "../../hateoas.js";
5
+ import { requirePermission } from "../../middleware/auth-middleware.js";
6
+ import { mutationResult, resolveActions, resolveOpRun, } from "../../route-helpers.js";
7
+ import { createComment, listComments, } from "../../services/operations/operation-run-comment-service.js";
8
8
  function commentResource(orderKey, runNo, seqNo) {
9
9
  return `orders/${orderKey}/runs/${runNo}/ops/${seqNo}/comments`;
10
10
  }