Permissions
A module can declare a permissions: tree — a nested set of access control
permissions the module contributes. Each permission has a key (its slug in the
generated identifier) and a name (a programmatic identifier the compiler uses to
name the generated const for it, e.g. name: managePosts generates
ManagePostsPermission). The identifier itself is either set explicitly via
fullKey: or computed by the compiler as {parent.fullKey}.{key} while the module
is preprocessed. If key is left empty too, it’s
normalized from name (e.g. name: managePosts with no key becomes key: managePosts).
name: blogpermissions: - key: post name: managePosts title: en: Manage posts description: en: Permissions related to blog posts children: - key: create name: createPost title: en: Create post - key: publish name: publishPost title: en: Publish postpost gets no explicit fullKey, so the compiler derives one: post. Its children
inherit that as their prefix, becoming post.create and post.publish. Because
post groups children and left its key for the compiler to derive, its generated
key gets a trailing .* — post.* — signaling it covers itself and everything below
it. A fullKey set explicitly is always used as-is, even on a node with children,
since the author chose that exact string on purpose.
Compiling this (with --tags typescript) produces a standalone Permissions.ts
with one exported const per root permission — not a single const wrapping the
whole forest: each root’s own key/name/title/description sit directly
alongside one property per child (named by the child’s own identifier), all
flattened onto the same object, so you can navigate
ManagePostsPermission.createPost.key with full editor autocomplete. Each literal
is asserted as const, so TypeScript infers its whole shape straight from it — no
separate type has to be hand-maintained in step with the tree. A root’s own const
name is always PascalCase with a trailing Permission (e.g. name: managePosts →
ManagePostsPermission, name: a → APermission) — never the bare, lowercase
child-style name — so a root reads unambiguously as a permission at a glance and two
unrelated single-letter roots can’t collide.
Alongside each root const, a <Name>PermissionList const flattens that same root
(and everything nested under it) into its own array — built by referencing the root
const’s own paths (e.g. ManagePostsPermission.createPost), not by re-declaring
separate literals, so it can never drift out of sync with it (mirrors the
<Name>List vars the Go compiler generates next to each root var). One list per
root, not a single list combining every root — a module with several unrelated
permission groups shouldn’t force every caller to filter one combined list back down
to the group it actually cares about. This is the only flattened form generated —
there’s no separate flat constant per node, since one would just be the same string
reachable a second, disconnected way (and would collide whenever two unrelated roots
happen to nest a child with the same name).
export const ManagePostsPermission = { key: "post.*", name: "managePosts", title: { en: "Manage posts" }, description: { en: "Permissions related to blog posts" }, createPost: { key: "post.create", name: "createPost", title: { en: "Create post" }, }, publishPost: { key: "post.publish", name: "publishPost", title: { en: "Publish post" }, },} as const;
export const ManagePostsPermissionList = [ ManagePostsPermission, ManagePostsPermission.createPost, ManagePostsPermission.publishPost,] as const;A module with a second, unrelated root permission gets its own separate const right
alongside it, e.g. export const ViewReportsPermission = { key: "report", ... } as const;, plus its own separate ViewReportsPermissionList — not folded into
ManagePostsPermissionList.
If a child’s own identifier happens to collide with one of the reserved attribute
names (key, name, title, description — e.g. a child literally named
key), that attribute on the parent is emitted with a trailing _ instead
(key_) so nothing gets silently overwritten.
Without --tags typescript, the same file is emitted as plain Permissions.js:
as const is omitted, and each root const is wrapped in Object.freeze(...)
instead, as a runtime guard against the tree being mutated after the fact.
export const ManagePostsPermission = Object.freeze({ key: "post.*", name: "managePosts", title: { en: "Manage posts" }, description: { en: "Permissions related to blog posts" }, createPost: { key: "post.create", name: "createPost", title: { en: "Create post" }, }, publishPost: { key: "post.publish", name: "publishPost", title: { en: "Publish post" }, },});
export const ManagePostsPermissionList = Object.freeze([ ManagePostsPermission, ManagePostsPermission.createPost, ManagePostsPermission.publishPost,]);