@things-factory/auth-base 10.1.6 → 10.1.7

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 (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist-server/index.js +8 -0
  3. package/dist-server/index.js.map +1 -1
  4. package/dist-server/service/index.d.ts +3 -1
  5. package/dist-server/service/index.js +15 -11
  6. package/dist-server/service/index.js.map +1 -1
  7. package/dist-server/service/privilege/privilege-directive.d.ts +10 -41
  8. package/dist-server/service/privilege/privilege-directive.js +14 -74
  9. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  10. package/dist-server/service/role/role-mutation.js +2 -2
  11. package/dist-server/service/role/role-mutation.js.map +1 -1
  12. package/dist-server/service/role/role-query.d.ts +43 -1
  13. package/dist-server/service/role/role-query.js +76 -33
  14. package/dist-server/service/role/role-query.js.map +1 -1
  15. package/dist-server/service/role-template/index.d.ts +4 -0
  16. package/dist-server/service/role-template/index.js +9 -0
  17. package/dist-server/service/role-template/index.js.map +1 -0
  18. package/dist-server/service/role-template/role-template-mutation.d.ts +39 -0
  19. package/dist-server/service/role-template/role-template-mutation.js +124 -0
  20. package/dist-server/service/role-template/role-template-mutation.js.map +1 -0
  21. package/dist-server/service/role-template/role-template-query.d.ts +11 -0
  22. package/dist-server/service/role-template/role-template-query.js +79 -0
  23. package/dist-server/service/role-template/role-template-query.js.map +1 -0
  24. package/dist-server/service/role-template/role-template-types.d.ts +51 -0
  25. package/dist-server/service/role-template/role-template-types.js +71 -0
  26. package/dist-server/service/role-template/role-template-types.js.map +1 -0
  27. package/dist-server/service/role-template/role-template.d.ts +110 -0
  28. package/dist-server/service/role-template/role-template.js +129 -0
  29. package/dist-server/service/role-template/role-template.js.map +1 -0
  30. package/dist-server/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +4 -4
  32. package/tests/privilege-directive.test.ts +1 -91
  33. package/tests/role-privileges-db.test.ts +98 -0
  34. package/tests/role-template-seed-db.test.ts +179 -0
  35. package/tests/role-template.test.ts +126 -0
@@ -4,7 +4,6 @@ exports.RoleQuery = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const typeorm_1 = require("typeorm");
6
6
  const type_graphql_1 = require("type-graphql");
7
- const env_1 = require("@things-factory/env");
8
7
  const shell_1 = require("@things-factory/shell");
9
8
  const privilege_js_1 = require("../privilege/privilege.js");
10
9
  const user_js_1 = require("../user/user.js");
@@ -68,28 +67,31 @@ let RoleQuery = class RoleQuery {
68
67
  assigned: assigned.has(id)
69
68
  }));
70
69
  }
70
+ /**
71
+ * 한 사람의 역할 — **부여할 수 있는 전체와 이 사람이 가진 것을 한 번에.**
72
+ *
73
+ * `rolePrivileges` 와 같은 모양이고, **같은 결함을 갖고 있었다.** 한 자리를 고쳤으면 두 번째
74
+ * 자리도 재야 한다는 것이 이 파일에서 그대로 났다.
75
+ *
76
+ * ① `UR.users_id` 자리에 `userId` 를 문자열로 이어 붙였다. `user:query` 를 가진
77
+ * 사람이 임의의 SQL 을 넣을 수 있다.
78
+ * ② `CASE WHEN … THEN true` 는 방언이다 — MSSQL 에 boolean 리터럴이 없다(DB 5종 지원).
79
+ * ③ `FROM roles` — **도메인을 안 봤다.** 남의 도메인 역할까지 전부 돌려줬다.
80
+ *
81
+ * ③ 을 고치면서 범위를 `role`·`rolePrivileges` 와 같게 뒀다 — 내 도메인과 부모 1단.
82
+ * 그것이 실제로 부여할 수 있는 범위다.
83
+ *
84
+ * 지금 이 질의를 부르는 화면은 없다(옛 `user-role-editor` 는 `user(email:)` 로 읽었다).
85
+ * 그래서 범위를 좁히는 것이 깨뜨릴 소비처가 없다.
86
+ */
71
87
  async userRoles(userId, context) {
72
- const userRoles = await (0, shell_1.getRepository)(role_js_1.Role).query(`
73
- SELECT
74
- id,
75
- name,
76
- description,
77
- CASE WHEN id IN (
78
- SELECT
79
- R.id
80
- FROM
81
- roles R JOIN users_roles UR
82
- ON
83
- R.id = UR.roles_id
84
- WHERE
85
- UR.users_id = '${userId}'
86
- ) THEN true
87
- ELSE false
88
- END AS assigned
89
- FROM
90
- roles
91
- `);
92
- return userRoles;
88
+ const { domain } = context.state;
89
+ const user = await (0, shell_1.getRepository)(user_js_1.User).findOne({ where: { id: userId }, relations: ['roles'] });
90
+ const assigned = new Set((user?.roles || []).map(role => role.id));
91
+ const roles = await (0, shell_1.getRepository)(role_js_1.Role).find({
92
+ where: { domain: { id: (0, typeorm_1.In)([domain.id, domain.parentId].filter(Boolean)) } }
93
+ });
94
+ return roles.map(({ id, name, description }) => ({ id, name, description, assigned: assigned.has(id) }));
93
95
  }
94
96
  async myRoles(context) {
95
97
  const { user, domain } = context.state;
@@ -103,10 +105,19 @@ let RoleQuery = class RoleQuery {
103
105
  .getMany();
104
106
  return roles;
105
107
  }
106
- async checkRolePreset(context) {
107
- const rolePreset = env_1.config.get('rolePreset') || [];
108
- return rolePreset;
109
- }
108
+ /*
109
+ * `checkRolePreset` stood here and has been removed. It read `config.get('rolePreset')`
110
+ * and returned it unchanged, declared as `[Role]`.
111
+ *
112
+ * Nothing set that config key and nothing called the query, across every repository on
113
+ * this machine — and it carried no `@privilege`, so any signed-in user could read
114
+ * whatever an installation later put there. Empty today, quietly public the day someone
115
+ * fills it. Role templates (`service/role-template/`) do what this was reaching for, as
116
+ * a boot registry rather than a config blob.
117
+ *
118
+ * auth-base ships to npm, so consumers outside this repository cannot be counted. The
119
+ * removal is recorded in the changelog — that is the only channel they have.
120
+ */
110
121
  async domain(role) {
111
122
  return await (0, shell_1.getRepository)(shell_1.Domain).findOneBy({ id: role.domainId });
112
123
  }
@@ -121,6 +132,38 @@ let RoleQuery = class RoleQuery {
121
132
  });
122
133
  return roleData?.users || [];
123
134
  }
135
+ /**
136
+ * 이 역할이 여는 권한이 **몇 개인가.**
137
+ *
138
+ * ── 왜 수만 따로 있나 ──────────────────────────────────────────────────────
139
+ * 목록 화면이 「이 역할이 무엇을 여나」를 말해야 하는데, 그것을 위해 권한 **행**을 받고 있었다.
140
+ *
141
+ * ```
142
+ * [확인함] roles(limit:20) 권한 포함 5,731 bytes · 69ms
143
+ * [확인함] roles(limit:20) 권한 없이 518 bytes · 7ms → 11.1배 · 10배
144
+ * ```
145
+ *
146
+ * 역할 2개에 권한 행 60개를 받아서 화면에는 잘린 코드 용어 셋을 그렸다. 역할 40개 ×
147
+ * 권한 100개면 한 페이지에 4,000행이다. **`privileges` 필드 리졸버가 역할마다 한 번씩 도는
148
+ * 것**까지 함께 없어지는 것이 더 큰 쪽이다.
149
+ *
150
+ * 그래서 목록은 이 칸을 쓴다 — 행을 안 받고 수를 받는다. 권한을 실제로 고치는 화면은
151
+ * `rolePrivileges(roleId:)` 로 따로 읽는다(전체 목록 + 이 역할이 가진 것을 한 번에).
152
+ *
153
+ * ── 왜 관계를 세지 않고 질의로 세나 ────────────────────────────────────────
154
+ * `relation(...).loadMany()` 는 행을 다 읽고 길이를 재는 것이라 여기서 고치려는 것을 그대로
155
+ * 다시 한다. `getCount()` 는 DB 가 센다.
156
+ *
157
+ * 조인 테이블의 주인은 `Privilege` 쪽이지만(`privilege.ts:72` 의 `@JoinTable`), 양쪽이 관계를
158
+ * 선언하고 있으므로 어느 쪽에서 조인해도 같다.
159
+ */
160
+ async privilegeCount(role) {
161
+ return await (0, shell_1.getRepository)(privilege_js_1.Privilege)
162
+ .createQueryBuilder('PRIVILEGE')
163
+ .innerJoin('PRIVILEGE.roles', 'ROLE')
164
+ .where('ROLE.id = :id', { id: role.id })
165
+ .getCount();
166
+ }
124
167
  async privileges(role) {
125
168
  return (await (0, shell_1.getRepository)(role_js_1.Role).findOne({
126
169
  where: { id: role.id },
@@ -178,13 +221,6 @@ tslib_1.__decorate([
178
221
  tslib_1.__metadata("design:paramtypes", [Object]),
179
222
  tslib_1.__metadata("design:returntype", Promise)
180
223
  ], RoleQuery.prototype, "myRoles", null);
181
- tslib_1.__decorate([
182
- (0, type_graphql_1.Query)(returns => [role_js_1.Role], { description: 'To fetch the preset of role for new user' }),
183
- tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
184
- tslib_1.__metadata("design:type", Function),
185
- tslib_1.__metadata("design:paramtypes", [Object]),
186
- tslib_1.__metadata("design:returntype", Promise)
187
- ], RoleQuery.prototype, "checkRolePreset", null);
188
224
  tslib_1.__decorate([
189
225
  (0, type_graphql_1.FieldResolver)(type => shell_1.Domain),
190
226
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
@@ -201,6 +237,13 @@ tslib_1.__decorate([
201
237
  tslib_1.__metadata("design:paramtypes", [role_js_1.Role, Object]),
202
238
  tslib_1.__metadata("design:returntype", Promise)
203
239
  ], RoleQuery.prototype, "users", null);
240
+ tslib_1.__decorate([
241
+ (0, type_graphql_1.FieldResolver)(type => type_graphql_1.Int),
242
+ tslib_1.__param(0, (0, type_graphql_1.Root)()),
243
+ tslib_1.__metadata("design:type", Function),
244
+ tslib_1.__metadata("design:paramtypes", [role_js_1.Role]),
245
+ tslib_1.__metadata("design:returntype", Promise)
246
+ ], RoleQuery.prototype, "privilegeCount", null);
204
247
  tslib_1.__decorate([
205
248
  (0, type_graphql_1.FieldResolver)(type => [privilege_js_1.Privilege]),
206
249
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
@@ -1 +1 @@
1
- {"version":3,"file":"role-query.js","sourceRoot":"","sources":["../../../server/service/role/role-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'\nimport { SelectQueryBuilder } from 'typeorm'\n\nimport { config } from '@things-factory/env'\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { User } from '../user/user.js'\nimport { Role } from './role.js'\nimport { RoleList, RolePrivilege, UserRole, RoleBrief } from './role-types.js'\n\n@Resolver(Role)\nexport class RoleQuery {\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => Role, { description: 'To fetch role' })\n async role(@Arg('name') name: string, @Ctx() context: ResolverContext): Promise<Role> {\n const { domain } = context.state\n return await getRepository(Role).findOneBy({\n domain: { id: In([domain.id, domain.parentId].filter(Boolean)) },\n name\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleList, { description: 'To fetch multiple users' })\n async roles(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<RoleList> {\n const { domain } = context.state\n\n const [items, total] = await getQueryBuilderFromListParams({\n repository: getRepository(Role),\n params,\n domain,\n searchables: ['name', 'description']\n }).getManyAndCount()\n\n return { items, total }\n }\n\n /**\n * 한 역할의 권한 — **전체 목록과 이 역할이 가진 것을 한 번에** 돌려준다.\n *\n * 권한 편집 화면이 필요한 것은 「무엇이 있나」와 「이 역할이 무엇을 갖나」 둘인데, 그것을\n * 질의 둘로 가르면 화면이 둘을 짝지어야 하고 그 짝짓기가 역할 **이름**으로 이루어졌다\n * (옛 `role-privilege-editor` — 같은 이름의 부모 도메인 역할과 헷갈렸다). 여기서 `roleId` 로\n * 짝지어 주면 화면이 그 일을 안 한다.\n *\n * ── 원시 SQL 을 걷어냈다 ────────────────────────────────────────────────────\n * 셋을 동시에 잃고 있었다.\n *\n * ① `roleId` 를 문자열로 이어 붙였다. `user:query` 를 가진 사람이 임의의 SQL 을 넣을 수 있다.\n * ② `CASE WHEN … THEN true` 는 방언이다 — MSSQL 에 boolean 리터럴이 없다. 이 저장소는\n * DB 5종을 지원한다.\n * ③ 도메인을 안 봤다. 남의 도메인 역할의 `roleId` 로도 답했다.\n *\n * 권한 마스터는 전체 배포에 84행 규모(도메인마다 늘지 않는다)이므로 관계를 그대로 읽는다.\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RolePrivilege], { description: 'To fetch privileges of a role' })\n async rolePrivileges(@Arg('roleId') roleId: string, @Ctx() context: ResolverContext): Promise<RolePrivilege[]> {\n const { domain } = context.state\n\n /* 상속으로 보이는 부모 도메인 역할까지가 범위다 — `role` 질의와 같은 규칙. */\n const role = await getRepository(Role).findOne({\n where: {\n id: roleId,\n domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }\n },\n relations: ['privileges']\n })\n\n if (!role) {\n return []\n }\n\n const assigned = new Set((role.privileges || []).map(privilege => privilege.id))\n\n return (await getRepository(Privilege).find()).map(({ id, name, category, description }) => ({\n id,\n name,\n category,\n description,\n assigned: assigned.has(id)\n }))\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [UserRole], { description: 'To fetch roles of a user' })\n async userRoles(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<UserRole[]> {\n const userRoles = await getRepository(Role).query(\n `\n SELECT\n id,\n name,\n description,\n CASE WHEN id IN (\n SELECT\n R.id\n FROM\n roles R JOIN users_roles UR\n ON\n R.id = UR.roles_id\n WHERE\n UR.users_id = '${userId}'\n ) THEN true\n ELSE false\n END AS assigned\n FROM\n roles\n `\n )\n\n return userRoles\n }\n\n @Query(returns => [RoleBrief], { description: 'To fetch roles of current user (id, name only)' })\n async myRoles(@Ctx() context: ResolverContext): Promise<RoleBrief[]> {\n const { user, domain } = context.state\n\n const roles = await getRepository(Role)\n .createQueryBuilder('ROLE')\n .leftJoin('ROLE.users', 'USER')\n .leftJoin('ROLE.domain', 'ROLE_DOMAIN')\n .where('USER.id = :userId', { userId: user.id })\n .andWhere('ROLE_DOMAIN.id In(:...domainIds)', { domainIds: [domain.id, domain.parentId].filter(Boolean) })\n .select(['ROLE.id', 'ROLE.name'])\n .getMany()\n\n return roles\n }\n\n @Query(returns => [Role], { description: 'To fetch the preset of role for new user' })\n async checkRolePreset(@Ctx() context: ResolverContext): Promise<Role[]> {\n const rolePreset = config.get('rolePreset') || []\n\n return rolePreset\n }\n\n @FieldResolver(type => Domain)\n async domain(@Root() role: Role) {\n return await getRepository(Domain).findOneBy({ id: role.domainId })\n }\n\n /**\n * Role의 users 필드를 위한 리졸버\n * 사용자 조회 권한이 있는 경우에만 접근 가능\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @FieldResolver(type => [User])\n async users(@Root() role: Role, @Ctx() context: ResolverContext) {\n const roleData = await getRepository(Role).findOne({\n where: { id: role.id },\n relations: ['users']\n })\n\n return roleData?.users || []\n }\n\n @FieldResolver(type => [Privilege])\n async privileges(@Root() role: Role) {\n return (\n await getRepository(Role).findOne({\n where: { id: role.id },\n relations: ['privileges']\n })\n ).privileges\n }\n\n @FieldResolver(type => User)\n async updater(@Root() role: Role): Promise<User> {\n return await getRepository(User).findOneBy({ id: role.updaterId })\n }\n\n @FieldResolver(type => User)\n async creator(@Root() role: Role): Promise<User> {\n return await getRepository(User).findOneBy({ id: role.creatorId })\n }\n}\n"]}
1
+ {"version":3,"file":"role-query.js","sourceRoot":"","sources":["../../../server/service/role/role-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Arg, Args, Ctx, Directive, FieldResolver, Int, Query, Resolver, Root } from 'type-graphql'\nimport { SelectQueryBuilder } from 'typeorm'\n\nimport { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { User } from '../user/user.js'\nimport { Role } from './role.js'\nimport { RoleList, RolePrivilege, UserRole, RoleBrief } from './role-types.js'\n\n@Resolver(Role)\nexport class RoleQuery {\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => Role, { description: 'To fetch role' })\n async role(@Arg('name') name: string, @Ctx() context: ResolverContext): Promise<Role> {\n const { domain } = context.state\n return await getRepository(Role).findOneBy({\n domain: { id: In([domain.id, domain.parentId].filter(Boolean)) },\n name\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleList, { description: 'To fetch multiple users' })\n async roles(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<RoleList> {\n const { domain } = context.state\n\n const [items, total] = await getQueryBuilderFromListParams({\n repository: getRepository(Role),\n params,\n domain,\n searchables: ['name', 'description']\n }).getManyAndCount()\n\n return { items, total }\n }\n\n /**\n * 한 역할의 권한 — **전체 목록과 이 역할이 가진 것을 한 번에** 돌려준다.\n *\n * 권한 편집 화면이 필요한 것은 「무엇이 있나」와 「이 역할이 무엇을 갖나」 둘인데, 그것을\n * 질의 둘로 가르면 화면이 둘을 짝지어야 하고 그 짝짓기가 역할 **이름**으로 이루어졌다\n * (옛 `role-privilege-editor` — 같은 이름의 부모 도메인 역할과 헷갈렸다). 여기서 `roleId` 로\n * 짝지어 주면 화면이 그 일을 안 한다.\n *\n * ── 원시 SQL 을 걷어냈다 ────────────────────────────────────────────────────\n * 셋을 동시에 잃고 있었다.\n *\n * ① `roleId` 를 문자열로 이어 붙였다. `user:query` 를 가진 사람이 임의의 SQL 을 넣을 수 있다.\n * ② `CASE WHEN … THEN true` 는 방언이다 — MSSQL 에 boolean 리터럴이 없다. 이 저장소는\n * DB 5종을 지원한다.\n * ③ 도메인을 안 봤다. 남의 도메인 역할의 `roleId` 로도 답했다.\n *\n * 권한 마스터는 전체 배포에 84행 규모(도메인마다 늘지 않는다)이므로 관계를 그대로 읽는다.\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RolePrivilege], { description: 'To fetch privileges of a role' })\n async rolePrivileges(@Arg('roleId') roleId: string, @Ctx() context: ResolverContext): Promise<RolePrivilege[]> {\n const { domain } = context.state\n\n /* 상속으로 보이는 부모 도메인 역할까지가 범위다 — `role` 질의와 같은 규칙. */\n const role = await getRepository(Role).findOne({\n where: {\n id: roleId,\n domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }\n },\n relations: ['privileges']\n })\n\n if (!role) {\n return []\n }\n\n const assigned = new Set((role.privileges || []).map(privilege => privilege.id))\n\n return (await getRepository(Privilege).find()).map(({ id, name, category, description }) => ({\n id,\n name,\n category,\n description,\n assigned: assigned.has(id)\n }))\n }\n\n /**\n * 한 사람의 역할 — **부여할 수 있는 전체와 이 사람이 가진 것을 한 번에.**\n *\n * `rolePrivileges` 와 같은 모양이고, **같은 결함을 갖고 있었다.** 한 자리를 고쳤으면 두 번째\n * 자리도 재야 한다는 것이 이 파일에서 그대로 났다.\n *\n * ① `UR.users_id` 자리에 `userId` 를 문자열로 이어 붙였다. `user:query` 를 가진\n * 사람이 임의의 SQL 을 넣을 수 있다.\n * ② `CASE WHEN … THEN true` 는 방언이다 — MSSQL 에 boolean 리터럴이 없다(DB 5종 지원).\n * ③ `FROM roles` — **도메인을 안 봤다.** 남의 도메인 역할까지 전부 돌려줬다.\n *\n * ③ 을 고치면서 범위를 `role`·`rolePrivileges` 와 같게 뒀다 — 내 도메인과 부모 1단.\n * 그것이 실제로 부여할 수 있는 범위다.\n *\n * 지금 이 질의를 부르는 화면은 없다(옛 `user-role-editor` 는 `user(email:)` 로 읽었다).\n * 그래서 범위를 좁히는 것이 깨뜨릴 소비처가 없다.\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [UserRole], { description: 'To fetch roles of a user' })\n async userRoles(@Arg('userId') userId: string, @Ctx() context: ResolverContext): Promise<UserRole[]> {\n const { domain } = context.state\n\n const user = await getRepository(User).findOne({ where: { id: userId }, relations: ['roles'] })\n const assigned = new Set((user?.roles || []).map(role => role.id))\n\n const roles = await getRepository(Role).find({\n where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) } }\n })\n\n return roles.map(({ id, name, description }) => ({ id, name, description, assigned: assigned.has(id) }))\n }\n\n @Query(returns => [RoleBrief], { description: 'To fetch roles of current user (id, name only)' })\n async myRoles(@Ctx() context: ResolverContext): Promise<RoleBrief[]> {\n const { user, domain } = context.state\n\n const roles = await getRepository(Role)\n .createQueryBuilder('ROLE')\n .leftJoin('ROLE.users', 'USER')\n .leftJoin('ROLE.domain', 'ROLE_DOMAIN')\n .where('USER.id = :userId', { userId: user.id })\n .andWhere('ROLE_DOMAIN.id In(:...domainIds)', { domainIds: [domain.id, domain.parentId].filter(Boolean) })\n .select(['ROLE.id', 'ROLE.name'])\n .getMany()\n\n return roles\n }\n\n /*\n * `checkRolePreset` stood here and has been removed. It read `config.get('rolePreset')`\n * and returned it unchanged, declared as `[Role]`.\n *\n * Nothing set that config key and nothing called the query, across every repository on\n * this machine — and it carried no `@privilege`, so any signed-in user could read\n * whatever an installation later put there. Empty today, quietly public the day someone\n * fills it. Role templates (`service/role-template/`) do what this was reaching for, as\n * a boot registry rather than a config blob.\n *\n * auth-base ships to npm, so consumers outside this repository cannot be counted. The\n * removal is recorded in the changelog — that is the only channel they have.\n */\n\n @FieldResolver(type => Domain)\n async domain(@Root() role: Role) {\n return await getRepository(Domain).findOneBy({ id: role.domainId })\n }\n\n /**\n * Role의 users 필드를 위한 리졸버\n * 사용자 조회 권한이 있는 경우에만 접근 가능\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @FieldResolver(type => [User])\n async users(@Root() role: Role, @Ctx() context: ResolverContext) {\n const roleData = await getRepository(Role).findOne({\n where: { id: role.id },\n relations: ['users']\n })\n\n return roleData?.users || []\n }\n\n /**\n * 이 역할이 여는 권한이 **몇 개인가.**\n *\n * ── 왜 수만 따로 있나 ──────────────────────────────────────────────────────\n * 목록 화면이 「이 역할이 무엇을 여나」를 말해야 하는데, 그것을 위해 권한 **행**을 받고 있었다.\n *\n * ```\n * [확인함] roles(limit:20) 권한 포함 5,731 bytes · 69ms\n * [확인함] roles(limit:20) 권한 없이 518 bytes · 7ms → 11.1배 · 10배\n * ```\n *\n * 역할 2개에 권한 행 60개를 받아서 화면에는 잘린 코드 용어 셋을 그렸다. 역할 40개 ×\n * 권한 100개면 한 페이지에 4,000행이다. **`privileges` 필드 리졸버가 역할마다 한 번씩 도는\n * 것**까지 함께 없어지는 것이 더 큰 쪽이다.\n *\n * 그래서 목록은 이 칸을 쓴다 — 행을 안 받고 수를 받는다. 권한을 실제로 고치는 화면은\n * `rolePrivileges(roleId:)` 로 따로 읽는다(전체 목록 + 이 역할이 가진 것을 한 번에).\n *\n * ── 왜 관계를 세지 않고 질의로 세나 ────────────────────────────────────────\n * `relation(...).loadMany()` 는 행을 다 읽고 길이를 재는 것이라 여기서 고치려는 것을 그대로\n * 다시 한다. `getCount()` 는 DB 가 센다.\n *\n * 조인 테이블의 주인은 `Privilege` 쪽이지만(`privilege.ts:72` 의 `@JoinTable`), 양쪽이 관계를\n * 선언하고 있으므로 어느 쪽에서 조인해도 같다.\n */\n @FieldResolver(type => Int)\n async privilegeCount(@Root() role: Role): Promise<number> {\n return await getRepository(Privilege)\n .createQueryBuilder('PRIVILEGE')\n .innerJoin('PRIVILEGE.roles', 'ROLE')\n .where('ROLE.id = :id', { id: role.id })\n .getCount()\n }\n\n @FieldResolver(type => [Privilege])\n async privileges(@Root() role: Role) {\n return (\n await getRepository(Role).findOne({\n where: { id: role.id },\n relations: ['privileges']\n })\n ).privileges\n }\n\n @FieldResolver(type => User)\n async updater(@Root() role: Role): Promise<User> {\n return await getRepository(User).findOneBy({ id: role.updaterId })\n }\n\n @FieldResolver(type => User)\n async creator(@Root() role: Role): Promise<User> {\n return await getRepository(User).findOneBy({ id: role.creatorId })\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { RoleTemplateMutation } from './role-template-mutation.js';
2
+ import { RoleTemplateQuery } from './role-template-query.js';
3
+ export declare const entities: any[];
4
+ export declare const resolvers: (typeof RoleTemplateQuery | typeof RoleTemplateMutation)[];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = exports.entities = void 0;
4
+ const role_template_mutation_js_1 = require("./role-template-mutation.js");
5
+ const role_template_query_js_1 = require("./role-template-query.js");
6
+ /* Templates live in the boot registry, not in a table, so there is no entity here. */
7
+ exports.entities = [];
8
+ exports.resolvers = [role_template_query_js_1.RoleTemplateQuery, role_template_mutation_js_1.RoleTemplateMutation];
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/role-template/index.ts"],"names":[],"mappings":";;;AAAA,2EAAkE;AAClE,qEAA4D;AAE5D,sFAAsF;AACzE,QAAA,QAAQ,GAAG,EAAE,CAAA;AACb,QAAA,SAAS,GAAG,CAAC,0CAAiB,EAAE,gDAAoB,CAAC,CAAA","sourcesContent":["import { RoleTemplateMutation } from './role-template-mutation.js'\nimport { RoleTemplateQuery } from './role-template-query.js'\n\n/* Templates live in the boot registry, not in a table, so there is no entity here. */\nexport const entities = []\nexport const resolvers = [RoleTemplateQuery, RoleTemplateMutation]\n"]}
@@ -0,0 +1,39 @@
1
+ import { Role } from '../role/role.js';
2
+ export declare class RoleTemplateMutation {
3
+ /**
4
+ * Creates a role from a template — once per name.
5
+ *
6
+ * ── Why an existing name is used rather than replaced ────────────────────────
7
+ * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`
8
+ * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the
9
+ * first one while the administrator edits the second, and nothing would report it: the
10
+ * screen shows two roles and the approvals go to the one nobody is looking at.
11
+ *
12
+ * So an existing role of that name is used as it stands. operato-plant's seed already
13
+ * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);
14
+ * this is the same rule, stated once.
15
+ *
16
+ * ── Why privileges are added and never removed ───────────────────────────────
17
+ * Two applications may declare a template with the same role name on purpose, and the
18
+ * privileges are then the union — to a user "quality inspector" is one seat. The union
19
+ * only widens: if a template later drops a privilege it stays on roles already seeded,
20
+ * because the seed runs once and the role belongs to the administrator afterwards.
21
+ *
22
+ * A privilege that is no longer declared anywhere is already handled elsewhere and not
23
+ * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses
24
+ * to switch it back on, and removing it is the administrator's call. Reclaiming it from
25
+ * this side would mean overwriting, and would be unable to tell a stale grant from one
26
+ * the administrator added on purpose.
27
+ */
28
+ createRoleFromTemplate(templateId: string,
29
+ /**
30
+ * What this plant calls the seat. Defaults to the template's name.
31
+ *
32
+ * The template knows which seat exists in a factory running this product; it does not
33
+ * know what the people there call it. And because a role is also what an approval line
34
+ * points at, the same seat is sometimes needed twice — a day shift and a night shift
35
+ * inspector hold the same privileges and take different approvals. Without a name of
36
+ * their own a template could be used exactly once per domain.
37
+ */
38
+ name: string | undefined, context: ResolverContext): Promise<Role>;
39
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RoleTemplateMutation = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const privilege_js_1 = require("../privilege/privilege.js");
7
+ const role_js_1 = require("../role/role.js");
8
+ const role_template_js_1 = require("./role-template.js");
9
+ let RoleTemplateMutation = class RoleTemplateMutation {
10
+ /**
11
+ * Creates a role from a template — once per name.
12
+ *
13
+ * ── Why an existing name is used rather than replaced ────────────────────────
14
+ * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`
15
+ * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the
16
+ * first one while the administrator edits the second, and nothing would report it: the
17
+ * screen shows two roles and the approvals go to the one nobody is looking at.
18
+ *
19
+ * So an existing role of that name is used as it stands. operato-plant's seed already
20
+ * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);
21
+ * this is the same rule, stated once.
22
+ *
23
+ * ── Why privileges are added and never removed ───────────────────────────────
24
+ * Two applications may declare a template with the same role name on purpose, and the
25
+ * privileges are then the union — to a user "quality inspector" is one seat. The union
26
+ * only widens: if a template later drops a privilege it stays on roles already seeded,
27
+ * because the seed runs once and the role belongs to the administrator afterwards.
28
+ *
29
+ * A privilege that is no longer declared anywhere is already handled elsewhere and not
30
+ * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses
31
+ * to switch it back on, and removing it is the administrator's call. Reclaiming it from
32
+ * this side would mean overwriting, and would be unable to tell a stale grant from one
33
+ * the administrator added on purpose.
34
+ */
35
+ async createRoleFromTemplate(templateId, name, context) {
36
+ const { domain, user, tx } = context.state;
37
+ const template = findTemplate(templateId);
38
+ if (!template) {
39
+ throw new Error(`There is no role template "${templateId}" in this installation.`);
40
+ }
41
+ const roleName = (name || template.name).trim();
42
+ if (!roleName) {
43
+ throw new Error('A role needs a name.');
44
+ }
45
+ const roleRepository = tx.getRepository(role_js_1.Role);
46
+ let role = await roleRepository.findOne({
47
+ where: { domain: { id: domain.id }, name: roleName },
48
+ relations: ['privileges']
49
+ });
50
+ if (!role) {
51
+ role = await roleRepository.save({
52
+ domain,
53
+ name: roleName,
54
+ description: template.description,
55
+ creator: user,
56
+ updater: user
57
+ });
58
+ role = await roleRepository.findOne({
59
+ where: { id: role.id },
60
+ relations: ['privileges']
61
+ });
62
+ }
63
+ const held = new Set((role.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`));
64
+ const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })));
65
+ /*
66
+ * Privileges have no domain — the whole server shares one set and what differs per
67
+ * domain is only which role holds them (`Privilege` has no domain column; auth-base's
68
+ * boot creates them by category and name alone). Filtering by domain finds nothing.
69
+ */
70
+ const privilegeRepository = tx.getRepository(privilege_js_1.Privilege);
71
+ const added = [];
72
+ for (const one of wanted) {
73
+ if (held.has(`${one.category} ${one.name}`)) {
74
+ continue;
75
+ }
76
+ const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } });
77
+ /*
78
+ * A privilege this installation does not declare is skipped, never created. Creating
79
+ * it here would make a row no `@privilege` asks for: the role would look as if it
80
+ * had been granted something, and open nothing. Granted-but-closed is the worst of
81
+ * the three outcomes. `roleTemplates` reports these as `missingCount` before the
82
+ * administrator picks the template.
83
+ */
84
+ if (privilege) {
85
+ added.push(privilege);
86
+ }
87
+ }
88
+ /*
89
+ * Nothing records that this role came from a template.
90
+ *
91
+ * A `templateVersions` column stood here so a later change to the template could be
92
+ * reported. It bought less than it looked like: what an administrator needs is "these
93
+ * two privileges were added to the template", and that is the template's current
94
+ * grants against the role's current privileges — computed, not stored. Telling them
95
+ * apart from a privilege the administrator removed on purpose would take the seeded
96
+ * content itself, not a version of it, and no screen asks for either yet.
97
+ *
98
+ * So the seed ends here. If reporting template drift is designed later, what to store
99
+ * follows from that design rather than being guessed at now.
100
+ */
101
+ role.privileges = [...(role.privileges || []), ...added];
102
+ role.updater = user;
103
+ return await roleRepository.save(role);
104
+ }
105
+ };
106
+ exports.RoleTemplateMutation = RoleTemplateMutation;
107
+ tslib_1.__decorate([
108
+ (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
109
+ (0, type_graphql_1.Directive)('@transaction'),
110
+ (0, type_graphql_1.Mutation)(returns => role_js_1.Role, { description: 'To create a role from a role template' }),
111
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('templateId')),
112
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('name', { nullable: true })),
113
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
114
+ tslib_1.__metadata("design:type", Function),
115
+ tslib_1.__metadata("design:paramtypes", [String, String, Object]),
116
+ tslib_1.__metadata("design:returntype", Promise)
117
+ ], RoleTemplateMutation.prototype, "createRoleFromTemplate", null);
118
+ exports.RoleTemplateMutation = RoleTemplateMutation = tslib_1.__decorate([
119
+ (0, type_graphql_1.Resolver)(role_js_1.Role)
120
+ ], RoleTemplateMutation);
121
+ function findTemplate(id) {
122
+ return (0, role_template_js_1.roleTemplateById)(id) || (0, role_template_js_1.computedRoleTemplates)().find(template => template.id === id);
123
+ }
124
+ //# sourceMappingURL=role-template-mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AAItE,4DAAqD;AACrD,6CAAsC;AACtC,yDAAoG;AAG7F,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IAIG,AAAN,KAAK,CAAC,sBAAsB,CACP,UAAkB,EAUJ,IAAwB,EAClD,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE1C,MAAM,QAAQ,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;QAEzC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,yBAAyB,CAAC,CAAA;QACpF,CAAC;QAED,MAAM,QAAQ,GAAG,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QAE/C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;QACzC,CAAC;QAED,MAAM,cAAc,GAAG,EAAE,CAAC,aAAa,CAAC,cAAI,CAAC,CAAA;QAE7C,IAAI,IAAI,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC;YACtC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YACpD,SAAS,EAAE,CAAC,YAAY,CAAC;SAC1B,CAAC,CAAA;QAEF,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC;gBAC/B,MAAM;gBACN,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,QAAQ,CAAC,WAAW;gBACjC,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,IAAI;aACP,CAAC,CAAA;YAET,IAAI,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC;gBAClC,KAAK,EAAE,EAAE,EAAE,EAAE,IAAK,CAAC,EAAE,EAAE;gBACvB,SAAS,EAAE,CAAC,YAAY,CAAC;aAC1B,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,IAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QAC1G,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAEnH;;;;WAIG;QACH,MAAM,mBAAmB,GAAG,EAAE,CAAC,aAAa,CAAC,wBAAS,CAAC,CAAA;QACvD,MAAM,KAAK,GAAgB,EAAE,CAAA;QAE7B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC5C,SAAQ;YACV,CAAC;YAED,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;YAE1G;;;;;;eAMG;YACH,IAAI,SAAS,EAAE,CAAC;gBACd,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YACvB,CAAC;QACH,CAAC;QAED;;;;;;;;;;;;WAYG;QACH,IAAK,CAAC,UAAU,GAAG,CAAC,GAAG,CAAC,IAAK,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAA;QAC1D,IAAK,CAAC,OAAO,GAAG,IAAI,CAAA;QAEpB,OAAO,MAAM,cAAc,CAAC,IAAI,CAAC,IAAK,CAAC,CAAA;IACzC,CAAC;CACF,CAAA;AA/HY,oDAAoB;AA6BzB;IAHL,IAAA,wBAAS,EAAC,uGAAuG,CAAC;IAClH,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,cAAI,EAAE,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;IAEjF,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IAUjB,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/B,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;kEAqFP;+BA9HU,oBAAoB;IADhC,IAAA,uBAAQ,EAAC,cAAI,CAAC;GACF,oBAAoB,CA+HhC;AAED,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,IAAA,mCAAgB,EAAC,EAAE,CAAC,IAAI,IAAA,wCAAqB,GAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;AAC7F,CAAC","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\n\n@Resolver(Role)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * Two applications may declare a template with the same role name on purpose, and the\n * privileges are then the union — to a user \"quality inspector\" is one seat. The union\n * only widens: if a template later drops a privilege it stays on roles already seeded,\n * because the seed runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (!role) {\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * Nothing records that this role came from a template.\n *\n * A `templateVersions` column stood here so a later change to the template could be\n * reported. It bought less than it looked like: what an administrator needs is \"these\n * two privileges were added to the template\", and that is the template's current\n * grants against the role's current privileges — computed, not stored. Telling them\n * apart from a privilege the administrator removed on purpose would take the seeded\n * content itself, not a version of it, and no screen asks for either yet.\n *\n * So the seed ends here. If reporting template drift is designed later, what to store\n * follows from that design rather than being guessed at now.\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { RoleTemplateView } from './role-template-types.js';
2
+ export declare class RoleTemplateQuery {
3
+ /**
4
+ * The templates this domain can start a role from.
5
+ *
6
+ * Gated the same way role editing is: choosing a template creates a role, so whoever may
7
+ * do this is whoever may create roles.
8
+ */
9
+ roleTemplates(context: ResolverContext): Promise<RoleTemplateView[]>;
10
+ roleTemplate(id: string, context: ResolverContext): Promise<RoleTemplateView | null>;
11
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RoleTemplateQuery = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const role_js_1 = require("../role/role.js");
8
+ const role_template_js_1 = require("./role-template.js");
9
+ const role_template_types_js_1 = require("./role-template-types.js");
10
+ let RoleTemplateQuery = class RoleTemplateQuery {
11
+ /**
12
+ * The templates this domain can start a role from.
13
+ *
14
+ * Gated the same way role editing is: choosing a template creates a role, so whoever may
15
+ * do this is whoever may create roles.
16
+ */
17
+ async roleTemplates(context) {
18
+ const { domain } = context.state;
19
+ /*
20
+ * Read the existing role names once. The alternative — a field resolver asking per
21
+ * template — runs a query per row for an answer the list already has.
22
+ */
23
+ const existing = new Set((await (0, shell_1.getRepository)(role_js_1.Role)
24
+ .createQueryBuilder('ROLE')
25
+ .select('ROLE.name')
26
+ .where('ROLE.domain = :domain', { domain: domain.id })
27
+ .getMany()).map(role => role.name));
28
+ return (0, role_template_js_1.allRoleTemplates)().map(template => view(template, existing));
29
+ }
30
+ async roleTemplate(id, context) {
31
+ const templates = await this.roleTemplates(context);
32
+ return templates.find(template => template.id === id) || null;
33
+ }
34
+ };
35
+ exports.RoleTemplateQuery = RoleTemplateQuery;
36
+ tslib_1.__decorate([
37
+ (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
38
+ (0, type_graphql_1.Query)(returns => [role_template_types_js_1.RoleTemplateView], { description: 'To fetch the role templates this installation offers' }),
39
+ tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
40
+ tslib_1.__metadata("design:type", Function),
41
+ tslib_1.__metadata("design:paramtypes", [Object]),
42
+ tslib_1.__metadata("design:returntype", Promise)
43
+ ], RoleTemplateQuery.prototype, "roleTemplates", null);
44
+ tslib_1.__decorate([
45
+ (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
46
+ (0, type_graphql_1.Query)(returns => role_template_types_js_1.RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' }),
47
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
48
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
49
+ tslib_1.__metadata("design:type", Function),
50
+ tslib_1.__metadata("design:paramtypes", [String, Object]),
51
+ tslib_1.__metadata("design:returntype", Promise)
52
+ ], RoleTemplateQuery.prototype, "roleTemplate", null);
53
+ exports.RoleTemplateQuery = RoleTemplateQuery = tslib_1.__decorate([
54
+ (0, type_graphql_1.Resolver)(role_template_types_js_1.RoleTemplateView)
55
+ ], RoleTemplateQuery);
56
+ function view(template, existing) {
57
+ const declared = process['PRIVILEGES'] || {};
58
+ const grants = template.grants.map(grant => {
59
+ const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`]);
60
+ const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`]);
61
+ return { category: grant.category, axes: present, missingAxes };
62
+ });
63
+ return {
64
+ id: template.id,
65
+ name: template.name,
66
+ description: template.description,
67
+ grants,
68
+ privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),
69
+ missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),
70
+ defaultNameTaken: existing.has(template.name),
71
+ /*
72
+ * Declared with no grants at all — an approval seat. Distinct from a template whose
73
+ * grants all went missing in this installation, which is a defect and shows a
74
+ * `missingCount` instead.
75
+ */
76
+ grantsNothingDeliberately: template.grants.length === 0
77
+ };
78
+ }
79
+ //# sourceMappingURL=role-template-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-template-query.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\nimport { RoleTemplateView } from './role-template-types.js'\n\n@Resolver(RoleTemplateView)\nexport class RoleTemplateQuery {\n /**\n * The templates this domain can start a role from.\n *\n * Gated the same way role editing is: choosing a template creates a role, so whoever may\n * do this is whoever may create roles.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RoleTemplateView], { description: 'To fetch the role templates this installation offers' })\n async roleTemplates(@Ctx() context: ResolverContext): Promise<RoleTemplateView[]> {\n const { domain } = context.state\n\n /*\n * Read the existing role names once. The alternative — a field resolver asking per\n * template — runs a query per row for an answer the list already has.\n */\n const existing = new Set(\n (\n await getRepository(Role)\n .createQueryBuilder('ROLE')\n .select('ROLE.name')\n .where('ROLE.domain = :domain', { domain: domain.id })\n .getMany()\n ).map(role => role.name)\n )\n\n return allRoleTemplates().map(template => view(template, existing))\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' })\n async roleTemplate(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<RoleTemplateView | null> {\n const templates = await this.roleTemplates(context)\n\n return templates.find(template => template.id === id) || null\n }\n}\n\nfunction view(template: RegisteredRoleTemplate, existing: Set<string>): RoleTemplateView {\n const declared = process['PRIVILEGES'] || {}\n\n const grants = template.grants.map(grant => {\n const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`])\n const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`])\n\n return { category: grant.category, axes: present, missingAxes }\n })\n\n return {\n id: template.id,\n name: template.name,\n description: template.description,\n grants,\n privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),\n missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),\n defaultNameTaken: existing.has(template.name),\n /*\n * Declared with no grants at all — an approval seat. Distinct from a template whose\n * grants all went missing in this installation, which is a defect and shows a\n * `missingCount` instead.\n */\n grantsNothingDeliberately: template.grants.length === 0\n }\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What a template looks like *to this installation*.
3
+ *
4
+ * The declared template and the offered template are not the same thing: an app declares
5
+ * grants across modules it expects to be installed, and a given installation has a subset.
6
+ * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47
7
+ * categories declared across the repository. So the answer has to say which parts of the
8
+ * template this installation can actually honour.
9
+ */
10
+ export declare class RoleTemplateGrantView {
11
+ category: string;
12
+ axes: string[];
13
+ /**
14
+ * Axes this installation does not declare.
15
+ *
16
+ * Dropping them quietly would hand the administrator a role that does not match the
17
+ * template's name, with no way to tell. They are reported instead, and the screen says
18
+ * "not present in this installation".
19
+ */
20
+ missingAxes: string[];
21
+ }
22
+ export declare class RoleTemplateView {
23
+ id: string;
24
+ name: string;
25
+ description?: string;
26
+ grants: RoleTemplateGrantView[];
27
+ privilegeCount: number;
28
+ missingCount: number;
29
+ /**
30
+ * Whether a role already carries this template's default name in the current domain.
31
+ *
32
+ * The screen warns before creating one, because seeding onto an existing name does not
33
+ * make a second role — approval lines hold a role's id, so a duplicate would leave the
34
+ * approval routed to the role nobody edits. The existing role is used as it stands and
35
+ * the template's privileges are added to it.
36
+ *
37
+ * It says "default name" rather than "seeded" because the administrator may give the
38
+ * role a name of their own, and then nothing here can tell whether this template was
39
+ * used. That is deliberate: provenance is not stored.
40
+ */
41
+ defaultNameTaken: boolean;
42
+ /**
43
+ * True when the template deliberately opens nothing.
44
+ *
45
+ * A seat that only receives approvals is legitimate — operato-twin declares seven roles
46
+ * with no privileges at all, used as the target an approval line points at. The screen
47
+ * must not draw that as "0 privileges, something is wrong", so the distinction is
48
+ * carried here rather than inferred from an empty list.
49
+ */
50
+ grantsNothingDeliberately: boolean;
51
+ }