@ysk-kit/cli 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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/dist/add-capability.d.ts +4 -0
  3. package/dist/add-capability.d.ts.map +1 -0
  4. package/dist/add-capability.js +104 -0
  5. package/dist/add-capability.js.map +1 -0
  6. package/dist/add-module.d.ts +5 -0
  7. package/dist/add-module.d.ts.map +1 -0
  8. package/dist/add-module.js +210 -0
  9. package/dist/add-module.js.map +1 -0
  10. package/dist/agent-stubs.d.ts +3 -0
  11. package/dist/agent-stubs.d.ts.map +1 -0
  12. package/dist/agent-stubs.js +25 -0
  13. package/dist/agent-stubs.js.map +1 -0
  14. package/dist/apply-capability.d.ts +3 -0
  15. package/dist/apply-capability.d.ts.map +1 -0
  16. package/dist/apply-capability.js +189 -0
  17. package/dist/apply-capability.js.map +1 -0
  18. package/dist/capability-catalog.d.ts +25 -0
  19. package/dist/capability-catalog.d.ts.map +1 -0
  20. package/dist/capability-catalog.js +123 -0
  21. package/dist/capability-catalog.js.map +1 -0
  22. package/dist/capability-patches.d.ts +40 -0
  23. package/dist/capability-patches.d.ts.map +1 -0
  24. package/dist/capability-patches.js +616 -0
  25. package/dist/capability-patches.js.map +1 -0
  26. package/dist/check-agent.d.ts +9 -0
  27. package/dist/check-agent.d.ts.map +1 -0
  28. package/dist/check-agent.js +85 -0
  29. package/dist/check-agent.js.map +1 -0
  30. package/dist/generate-openapi.d.ts +2 -0
  31. package/dist/generate-openapi.d.ts.map +1 -0
  32. package/dist/generate-openapi.js +52 -0
  33. package/dist/generate-openapi.js.map +1 -0
  34. package/dist/help.d.ts +2 -0
  35. package/dist/help.d.ts.map +1 -0
  36. package/dist/help.js +54 -0
  37. package/dist/help.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +59 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/merge-prisma.d.ts +15 -0
  43. package/dist/merge-prisma.d.ts.map +1 -0
  44. package/dist/merge-prisma.js +90 -0
  45. package/dist/merge-prisma.js.map +1 -0
  46. package/dist/patch-text.d.ts +17 -0
  47. package/dist/patch-text.d.ts.map +1 -0
  48. package/dist/patch-text.js +72 -0
  49. package/dist/patch-text.js.map +1 -0
  50. package/dist/upgrade.d.ts +16 -0
  51. package/dist/upgrade.d.ts.map +1 -0
  52. package/dist/upgrade.js +110 -0
  53. package/dist/upgrade.js.map +1 -0
  54. package/package.json +41 -0
  55. package/templates/agent/skills/add-capability/SKILL.md +8 -0
  56. package/templates/agent/skills/add-module/SKILL.md +8 -0
  57. package/templates/agent/skills/envelope-api/SKILL.md +8 -0
  58. package/templates/agent/skills/fix-layers/SKILL.md +8 -0
  59. package/templates/agent/skills/new-product/SKILL.md +8 -0
  60. package/templates/agent/skills/verify-change/SKILL.md +8 -0
  61. package/templates/agent/ysk-kit.mdc +6 -0
  62. package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.ts +140 -0
  63. package/templates/capabilities/billing/apps/api/src/modules/billing/domain/billing-port.ts +23 -0
  64. package/templates/capabilities/billing/apps/api/src/modules/billing/domain/subscription-repository.ts +22 -0
  65. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/billing-router.ts +85 -0
  66. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/billing.app.test.ts +273 -0
  67. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/log-billing.ts +31 -0
  68. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/memory-subscription-repository.ts +27 -0
  69. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/prisma-subscription-repository.ts +52 -0
  70. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.test.ts +127 -0
  71. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.ts +160 -0
  72. package/templates/capabilities/billing/apps/web/src/features/orgs/billing-page.tsx +183 -0
  73. package/templates/capabilities/billing/packages/contracts/src/api/billing.ts +85 -0
  74. package/templates/capabilities/billing/packages/contracts/src/dto/billing.ts +59 -0
  75. package/templates/capabilities/billing/packages/sdk/src/resources/billing.ts +38 -0
  76. package/templates/capabilities/billing/packages/web-sdk/src/billing-hooks.ts +56 -0
  77. package/templates/capabilities/llm/apps/api/src/modules/llm/application/llm-service.ts +48 -0
  78. package/templates/capabilities/llm/apps/api/src/modules/llm/domain/usage-repository.ts +10 -0
  79. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/llm-router.ts +88 -0
  80. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/llm.app.test.ts +89 -0
  81. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/memory-usage-repository.ts +13 -0
  82. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/prisma-usage-repository.ts +17 -0
  83. package/templates/capabilities/llm/apps/web/src/features/llm/llm-page.tsx +53 -0
  84. package/templates/capabilities/llm/packages/contracts/src/api/llm.ts +26 -0
  85. package/templates/capabilities/llm/packages/contracts/src/dto/llm.ts +33 -0
  86. package/templates/capabilities/llm/packages/sdk/src/resources/llm.ts +68 -0
  87. package/templates/capabilities/llm/packages/web-sdk/src/llm-hooks.ts +12 -0
  88. package/templates/capabilities/push/apps/api/src/modules/devices/application/device-service.ts +42 -0
  89. package/templates/capabilities/push/apps/api/src/modules/devices/domain/device-repository.ts +22 -0
  90. package/templates/capabilities/push/apps/api/src/modules/devices/infra/device-router.ts +42 -0
  91. package/templates/capabilities/push/apps/api/src/modules/devices/infra/device.app.test.ts +34 -0
  92. package/templates/capabilities/push/apps/api/src/modules/devices/infra/memory-device-repository.ts +33 -0
  93. package/templates/capabilities/push/apps/api/src/modules/devices/infra/prisma-device-repository.ts +42 -0
  94. package/templates/capabilities/push/apps/mobile/src/adapters/push.ts +21 -0
  95. package/templates/capabilities/push/packages/contracts/src/api/devices.ts +33 -0
  96. package/templates/capabilities/push/packages/contracts/src/dto/device.ts +18 -0
  97. package/templates/capabilities/push/packages/sdk/src/resources/devices.ts +10 -0
  98. package/templates/capabilities/team/apps/api/src/modules/organizations/application/organization-service.ts +272 -0
  99. package/templates/capabilities/team/apps/api/src/modules/organizations/domain/organization-repository.ts +58 -0
  100. package/templates/capabilities/team/apps/api/src/modules/organizations/infra/memory-organization-repository.ts +141 -0
  101. package/templates/capabilities/team/apps/api/src/modules/organizations/infra/organization-router.ts +130 -0
  102. package/templates/capabilities/team/apps/api/src/modules/organizations/infra/organization.app.test.ts +213 -0
  103. package/templates/capabilities/team/apps/api/src/modules/organizations/infra/prisma-organization-repository.ts +142 -0
  104. package/templates/capabilities/team/apps/mobile/src/screens/invite-screen.tsx +40 -0
  105. package/templates/capabilities/team/apps/mobile/src/screens/org-detail-screen.tsx +154 -0
  106. package/templates/capabilities/team/apps/mobile/src/screens/orgs-screen.tsx +53 -0
  107. package/templates/capabilities/team/apps/web/src/features/orgs/invite-page.tsx +77 -0
  108. package/templates/capabilities/team/apps/web/src/features/orgs/org-detail-page.tsx +138 -0
  109. package/templates/capabilities/team/apps/web/src/features/orgs/orgs-page.tsx +73 -0
  110. package/templates/capabilities/team/packages/contracts/src/api/organizations.ts +154 -0
  111. package/templates/capabilities/team/packages/contracts/src/dto/organization.ts +58 -0
  112. package/templates/capabilities/team/packages/sdk/src/resources/organizations.ts +53 -0
  113. package/templates/capabilities/team/packages/web-sdk/src/organizations-hooks.ts +80 -0
  114. package/templates/module/api.ts.tmpl +32 -0
  115. package/templates/module/application-service.ts.tmpl +9 -0
  116. package/templates/module/domain-repository.ts.tmpl +15 -0
  117. package/templates/module/dto.ts.tmpl +21 -0
  118. package/templates/module/memory-repository.ts.tmpl +36 -0
  119. package/templates/module/model.prisma.tmpl +8 -0
  120. package/templates/module/prisma-repository.ts.tmpl +38 -0
  121. package/templates/module/router.ts.tmpl +32 -0
  122. package/templates/module/sdk-resource.ts.tmpl +17 -0
  123. package/templates/module/service.test.ts.tmpl +33 -0
  124. package/templates/module/web-hooks.ts.tmpl +24 -0
  125. package/templates/module/web-page.tsx.tmpl +69 -0
@@ -0,0 +1,138 @@
1
+ import { useNavigate } from '@tanstack/react-router';
2
+ import { InviteMemberCommandSchema } from '@ysk-kit/contracts';
3
+ import {
4
+ Button,
5
+ Input,
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@ysk-kit/ui';
13
+ import { orgRoleCan } from '@ysk-kit/ui-logic';
14
+ import { createOrganizationHooks } from '@ysk-kit/web-sdk';
15
+ import { type FormEvent, useState } from 'react';
16
+ import { api, userHooks } from '../../lib/client';
17
+
18
+ const orgHooks = createOrganizationHooks(api);
19
+
20
+ export function OrgDetailPage({ organizationId }: { organizationId: string }) {
21
+ const me = userHooks.useMe();
22
+ const org = orgHooks.useOrganization(organizationId);
23
+ const members = orgHooks.useOrganizationMembers(organizationId);
24
+ const myRole = members.data?.find((row) => row.userId === me.data?.id)?.role;
25
+ const canInvite = myRole ? orgRoleCan(myRole, 'org.invite') : false;
26
+ const canRemove = myRole ? orgRoleCan(myRole, 'org.member.remove') : false;
27
+ const canBill = myRole ? orgRoleCan(myRole, 'org.billing') : false;
28
+ const invites = orgHooks.useOrganizationInvites(organizationId, canInvite);
29
+ const invite = orgHooks.useInviteMember(organizationId);
30
+ const removeMember = orgHooks.useRemoveMember(organizationId);
31
+ const leave = orgHooks.useLeaveOrganization();
32
+ const navigate = useNavigate();
33
+ const [email, setEmail] = useState('');
34
+ const [formError, setFormError] = useState<string | null>(null);
35
+
36
+ const onInvite = (event: FormEvent) => {
37
+ event.preventDefault();
38
+ const parsed = InviteMemberCommandSchema.safeParse({ email, role: 'MEMBER' });
39
+ if (!parsed.success) {
40
+ setFormError(parsed.error.issues[0]?.message ?? 'Invalid');
41
+ return;
42
+ }
43
+ setFormError(null);
44
+ invite.mutate(parsed.data, {
45
+ onSuccess: () => setEmail(''),
46
+ onError: (error) => setFormError(error.message),
47
+ });
48
+ };
49
+
50
+ return (
51
+ <section className="space-y-6">
52
+ <h1 className="text-2xl font-semibold">{org.data?.name ?? 'Organization'}</h1>
53
+ {canBill ? (
54
+ <a
55
+ href={`/orgs/${organizationId}/billing`}
56
+ className="text-sm text-zinc-600 hover:text-zinc-900"
57
+ >
58
+ Billing
59
+ </a>
60
+ ) : null}
61
+ {formError ? <p className="text-sm text-red-600">{formError}</p> : null}
62
+ {canInvite ? (
63
+ <form onSubmit={onInvite} className="flex flex-wrap items-end gap-3">
64
+ <label className="grid gap-1 text-sm" htmlFor="invite-email">
65
+ Invite email
66
+ <Input
67
+ id="invite-email"
68
+ type="email"
69
+ value={email}
70
+ onChange={(e) => setEmail(e.target.value)}
71
+ required
72
+ />
73
+ </label>
74
+ <Button type="submit" disabled={invite.isPending}>
75
+ Invite
76
+ </Button>
77
+ </form>
78
+ ) : null}
79
+ <Table>
80
+ <TableHeader>
81
+ <TableRow>
82
+ <TableHead>Name</TableHead>
83
+ <TableHead>Email</TableHead>
84
+ <TableHead>Role</TableHead>
85
+ <TableHead />
86
+ </TableRow>
87
+ </TableHeader>
88
+ <TableBody>
89
+ {(members.data ?? []).map((member) => (
90
+ <TableRow key={member.id}>
91
+ <TableCell>{member.displayName}</TableCell>
92
+ <TableCell>{member.email}</TableCell>
93
+ <TableCell>{member.role}</TableCell>
94
+ <TableCell>
95
+ {canRemove && member.userId !== me.data?.id ? (
96
+ <Button
97
+ type="button"
98
+ variant="outline"
99
+ size="sm"
100
+ onClick={() => removeMember.mutate(member.userId)}
101
+ >
102
+ Remove
103
+ </Button>
104
+ ) : null}
105
+ </TableCell>
106
+ </TableRow>
107
+ ))}
108
+ </TableBody>
109
+ </Table>
110
+ {canInvite && (invites.data?.length ?? 0) > 0 ? (
111
+ <div className="space-y-2">
112
+ <h2 className="text-lg font-medium">Pending invites</h2>
113
+ <ul className="text-sm text-zinc-600">
114
+ {(invites.data ?? []).map((row) => (
115
+ <li key={row.id}>
116
+ {row.email} · {row.role}
117
+ </li>
118
+ ))}
119
+ </ul>
120
+ </div>
121
+ ) : null}
122
+ <Button
123
+ type="button"
124
+ variant="outline"
125
+ onClick={() =>
126
+ leave.mutate(organizationId, {
127
+ onSuccess: () => {
128
+ void navigate({ to: '/orgs' });
129
+ },
130
+ onError: (error) => setFormError(error.message),
131
+ })
132
+ }
133
+ >
134
+ Leave
135
+ </Button>
136
+ </section>
137
+ );
138
+ }
@@ -0,0 +1,73 @@
1
+ import { Link } from '@tanstack/react-router';
2
+ import { CreateOrganizationCommandSchema } from '@ysk-kit/contracts';
3
+ import {
4
+ Button,
5
+ Input,
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@ysk-kit/ui';
13
+ import { createOrganizationHooks } from '@ysk-kit/web-sdk';
14
+ import { type FormEvent, useState } from 'react';
15
+ import { api } from '../../lib/client';
16
+
17
+ const orgHooks = createOrganizationHooks(api);
18
+
19
+ export function OrgsPage() {
20
+ const orgs = orgHooks.useOrganizations();
21
+ const createOrg = orgHooks.useCreateOrganization();
22
+ const [name, setName] = useState('');
23
+ const [formError, setFormError] = useState<string | null>(null);
24
+
25
+ const onSubmit = (event: FormEvent) => {
26
+ event.preventDefault();
27
+ const parsed = CreateOrganizationCommandSchema.safeParse({ name });
28
+ if (!parsed.success) {
29
+ setFormError(parsed.error.issues[0]?.message ?? 'Invalid');
30
+ return;
31
+ }
32
+ setFormError(null);
33
+ createOrg.mutate(parsed.data, {
34
+ onSuccess: () => setName(''),
35
+ onError: (error) => setFormError(error.message),
36
+ });
37
+ };
38
+
39
+ return (
40
+ <section className="space-y-6">
41
+ <h1 className="text-2xl font-semibold">Organizations</h1>
42
+ <form onSubmit={onSubmit} className="flex flex-wrap items-end gap-3">
43
+ <label className="grid gap-1 text-sm" htmlFor="org-name">
44
+ Name
45
+ <Input id="org-name" value={name} onChange={(e) => setName(e.target.value)} required />
46
+ </label>
47
+ <Button type="submit" disabled={createOrg.isPending}>
48
+ Create
49
+ </Button>
50
+ </form>
51
+ {formError ? <p className="text-sm text-red-600">{formError}</p> : null}
52
+ {orgs.error ? <p className="text-sm text-red-600">{orgs.error.message}</p> : null}
53
+ <Table>
54
+ <TableHeader>
55
+ <TableRow>
56
+ <TableHead>Name</TableHead>
57
+ </TableRow>
58
+ </TableHeader>
59
+ <TableBody>
60
+ {(orgs.data ?? []).map((org) => (
61
+ <TableRow key={org.id}>
62
+ <TableCell>
63
+ <Link to="/orgs/$organizationId" params={{ organizationId: org.id }}>
64
+ {org.name}
65
+ </Link>
66
+ </TableCell>
67
+ </TableRow>
68
+ ))}
69
+ </TableBody>
70
+ </Table>
71
+ </section>
72
+ );
73
+ }
@@ -0,0 +1,154 @@
1
+ import { initContract } from '@ts-rest/core';
2
+ import { z } from 'zod';
3
+ import {
4
+ AcceptedInviteDtoSchema,
5
+ AcceptInviteCommandSchema,
6
+ CreateOrganizationCommandSchema,
7
+ InviteMemberCommandSchema,
8
+ MembershipDtoSchema,
9
+ OrganizationDtoSchema,
10
+ OrgInviteDtoSchema,
11
+ UpdateOrganizationCommandSchema,
12
+ } from '../dto/organization';
13
+ import { ErrSchema, OkSchema } from '../errors/envelope';
14
+
15
+ const c = initContract();
16
+ const orgIdParams = z.object({ organizationId: z.string().uuid() });
17
+ const memberParams = z.object({
18
+ organizationId: z.string().uuid(),
19
+ userId: z.string().uuid(),
20
+ });
21
+ const inviteParams = z.object({
22
+ organizationId: z.string().uuid(),
23
+ id: z.string().uuid(),
24
+ });
25
+
26
+ export const organizationsContract = c.router({
27
+ create: {
28
+ method: 'POST',
29
+ path: '/v1/organizations',
30
+ body: CreateOrganizationCommandSchema,
31
+ responses: { 201: OkSchema(OrganizationDtoSchema), 401: ErrSchema, 422: ErrSchema },
32
+ summary: 'Create an organization',
33
+ },
34
+ list: {
35
+ method: 'GET',
36
+ path: '/v1/organizations',
37
+ responses: { 200: OkSchema(z.array(OrganizationDtoSchema)), 401: ErrSchema },
38
+ summary: 'List my organizations',
39
+ },
40
+ acceptInvite: {
41
+ method: 'POST',
42
+ path: '/v1/organizations/invites/accept',
43
+ body: AcceptInviteCommandSchema,
44
+ responses: {
45
+ 200: OkSchema(AcceptedInviteDtoSchema),
46
+ 401: ErrSchema,
47
+ 409: ErrSchema,
48
+ 422: ErrSchema,
49
+ },
50
+ summary: 'Accept an organization invite',
51
+ },
52
+ get: {
53
+ method: 'GET',
54
+ path: '/v1/organizations/:organizationId',
55
+ pathParams: orgIdParams,
56
+ responses: {
57
+ 200: OkSchema(OrganizationDtoSchema),
58
+ 401: ErrSchema,
59
+ 403: ErrSchema,
60
+ 404: ErrSchema,
61
+ },
62
+ summary: 'Get an organization',
63
+ },
64
+ update: {
65
+ method: 'PATCH',
66
+ path: '/v1/organizations/:organizationId',
67
+ pathParams: orgIdParams,
68
+ body: UpdateOrganizationCommandSchema,
69
+ responses: {
70
+ 200: OkSchema(OrganizationDtoSchema),
71
+ 401: ErrSchema,
72
+ 403: ErrSchema,
73
+ 404: ErrSchema,
74
+ 422: ErrSchema,
75
+ },
76
+ summary: 'Rename an organization',
77
+ },
78
+ members: {
79
+ method: 'GET',
80
+ path: '/v1/organizations/:organizationId/members',
81
+ pathParams: orgIdParams,
82
+ responses: {
83
+ 200: OkSchema(z.array(MembershipDtoSchema)),
84
+ 401: ErrSchema,
85
+ 403: ErrSchema,
86
+ 404: ErrSchema,
87
+ },
88
+ summary: 'List members',
89
+ },
90
+ invite: {
91
+ method: 'POST',
92
+ path: '/v1/organizations/:organizationId/invites',
93
+ pathParams: orgIdParams,
94
+ body: InviteMemberCommandSchema,
95
+ responses: {
96
+ 200: OkSchema(OrgInviteDtoSchema),
97
+ 401: ErrSchema,
98
+ 403: ErrSchema,
99
+ 404: ErrSchema,
100
+ 409: ErrSchema,
101
+ 422: ErrSchema,
102
+ },
103
+ summary: 'Invite a member by email',
104
+ },
105
+ invites: {
106
+ method: 'GET',
107
+ path: '/v1/organizations/:organizationId/invites',
108
+ pathParams: orgIdParams,
109
+ responses: {
110
+ 200: OkSchema(z.array(OrgInviteDtoSchema)),
111
+ 401: ErrSchema,
112
+ 403: ErrSchema,
113
+ 404: ErrSchema,
114
+ },
115
+ summary: 'List pending invites',
116
+ },
117
+ revokeInvite: {
118
+ method: 'DELETE',
119
+ path: '/v1/organizations/:organizationId/invites/:id',
120
+ pathParams: inviteParams,
121
+ responses: {
122
+ 200: OkSchema(z.object({ removed: z.literal(true) })),
123
+ 401: ErrSchema,
124
+ 403: ErrSchema,
125
+ 404: ErrSchema,
126
+ },
127
+ summary: 'Revoke a pending invite',
128
+ },
129
+ removeMember: {
130
+ method: 'DELETE',
131
+ path: '/v1/organizations/:organizationId/members/:userId',
132
+ pathParams: memberParams,
133
+ responses: {
134
+ 200: OkSchema(z.object({ removed: z.literal(true) })),
135
+ 401: ErrSchema,
136
+ 403: ErrSchema,
137
+ 404: ErrSchema,
138
+ },
139
+ summary: 'Remove a member',
140
+ },
141
+ leave: {
142
+ method: 'POST',
143
+ path: '/v1/organizations/:organizationId/leave',
144
+ pathParams: orgIdParams,
145
+ body: z.object({}).optional(),
146
+ responses: {
147
+ 200: OkSchema(z.object({ left: z.literal(true) })),
148
+ 401: ErrSchema,
149
+ 403: ErrSchema,
150
+ 404: ErrSchema,
151
+ },
152
+ summary: 'Leave an organization',
153
+ },
154
+ });
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import { InviteOrgRoleSchema, OrgRoleSchema } from '../enums/org-role';
3
+
4
+ export const OrganizationDtoSchema = z.object({
5
+ id: z.string().uuid(),
6
+ name: z.string().min(1).max(80),
7
+ createdAt: z.iso.datetime(),
8
+ });
9
+ export type OrganizationDto = z.infer<typeof OrganizationDtoSchema>;
10
+
11
+ export const MembershipDtoSchema = z.object({
12
+ id: z.string().uuid(),
13
+ organizationId: z.string().uuid(),
14
+ userId: z.string().uuid(),
15
+ email: z.string().email().nullable(),
16
+ displayName: z.string().min(1).max(80),
17
+ role: OrgRoleSchema,
18
+ createdAt: z.iso.datetime(),
19
+ });
20
+ export type MembershipDto = z.infer<typeof MembershipDtoSchema>;
21
+
22
+ export const OrgInviteDtoSchema = z.object({
23
+ id: z.string().uuid(),
24
+ organizationId: z.string().uuid(),
25
+ email: z.string().email(),
26
+ role: InviteOrgRoleSchema,
27
+ createdAt: z.iso.datetime(),
28
+ expiresAt: z.iso.datetime(),
29
+ });
30
+ export type OrgInviteDto = z.infer<typeof OrgInviteDtoSchema>;
31
+
32
+ export const CreateOrganizationCommandSchema = z.object({
33
+ name: z.string().min(1).max(80),
34
+ });
35
+ export type CreateOrganizationCommand = z.infer<typeof CreateOrganizationCommandSchema>;
36
+
37
+ export const UpdateOrganizationCommandSchema = z.object({
38
+ name: z.string().min(1).max(80),
39
+ });
40
+ export type UpdateOrganizationCommand = z.infer<typeof UpdateOrganizationCommandSchema>;
41
+
42
+ export const InviteMemberCommandSchema = z.object({
43
+ email: z.string().email(),
44
+ role: InviteOrgRoleSchema,
45
+ });
46
+ export type InviteMemberCommand = z.infer<typeof InviteMemberCommandSchema>;
47
+
48
+ export const AcceptInviteCommandSchema = z.object({
49
+ token: z.string().min(16),
50
+ password: z.string().min(8).max(128).optional(),
51
+ displayName: z.string().min(1).max(80).optional(),
52
+ });
53
+ export type AcceptInviteCommand = z.infer<typeof AcceptInviteCommandSchema>;
54
+
55
+ export const AcceptedInviteDtoSchema = z.object({
56
+ accepted: z.literal(true),
57
+ });
58
+ export type AcceptedInviteDto = z.infer<typeof AcceptedInviteDtoSchema>;
@@ -0,0 +1,53 @@
1
+ import type {
2
+ AcceptInviteCommand,
3
+ CreateOrganizationCommand,
4
+ InviteMemberCommand,
5
+ MembershipDto,
6
+ OrganizationDto,
7
+ OrgInviteDto,
8
+ UpdateOrganizationCommand,
9
+ } from '@ysk-kit/contracts';
10
+ import type { HttpClient } from '../http';
11
+
12
+ export const organizationsResource = (http: HttpClient) => ({
13
+ create: (body: CreateOrganizationCommand) =>
14
+ http.request<OrganizationDto>('/v1/organizations', {
15
+ method: 'POST',
16
+ body: JSON.stringify(body),
17
+ }),
18
+ list: () => http.request<OrganizationDto[]>('/v1/organizations'),
19
+ get: (organizationId: string) =>
20
+ http.request<OrganizationDto>(`/v1/organizations/${organizationId}`),
21
+ update: (organizationId: string, body: UpdateOrganizationCommand) =>
22
+ http.request<OrganizationDto>(`/v1/organizations/${organizationId}`, {
23
+ method: 'PATCH',
24
+ body: JSON.stringify(body),
25
+ }),
26
+ members: (organizationId: string) =>
27
+ http.request<MembershipDto[]>(`/v1/organizations/${organizationId}/members`),
28
+ invite: (organizationId: string, body: InviteMemberCommand) =>
29
+ http.request<OrgInviteDto>(`/v1/organizations/${organizationId}/invites`, {
30
+ method: 'POST',
31
+ body: JSON.stringify(body),
32
+ }),
33
+ invites: (organizationId: string) =>
34
+ http.request<OrgInviteDto[]>(`/v1/organizations/${organizationId}/invites`),
35
+ revokeInvite: (organizationId: string, id: string) =>
36
+ http.request<{ removed: true }>(`/v1/organizations/${organizationId}/invites/${id}`, {
37
+ method: 'DELETE',
38
+ }),
39
+ removeMember: (organizationId: string, userId: string) =>
40
+ http.request<{ removed: true }>(`/v1/organizations/${organizationId}/members/${userId}`, {
41
+ method: 'DELETE',
42
+ }),
43
+ leave: (organizationId: string) =>
44
+ http.request<{ left: true }>(`/v1/organizations/${organizationId}/leave`, {
45
+ method: 'POST',
46
+ body: '{}',
47
+ }),
48
+ acceptInvite: (body: AcceptInviteCommand) =>
49
+ http.request<{ accepted: true }>('/v1/organizations/invites/accept', {
50
+ method: 'POST',
51
+ body: JSON.stringify(body),
52
+ }),
53
+ });
@@ -0,0 +1,80 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import type {
3
+ AcceptInviteCommand,
4
+ CreateOrganizationCommand,
5
+ InviteMemberCommand,
6
+ } from '@ysk-kit/contracts';
7
+ import type { YskClient } from '@ysk-kit/sdk';
8
+
9
+ export const organizationsQueryKey = ['organizations'] as const;
10
+ export const organizationQueryKey = (id: string) => ['organization', id] as const;
11
+
12
+ export function createOrganizationHooks(client: YskClient) {
13
+ return {
14
+ useOrganizations: () =>
15
+ useQuery({
16
+ queryKey: organizationsQueryKey,
17
+ queryFn: () => client.organizations.list(),
18
+ }),
19
+ useOrganization: (id: string) =>
20
+ useQuery({
21
+ queryKey: organizationQueryKey(id),
22
+ queryFn: () => client.organizations.get(id),
23
+ enabled: id.length > 0,
24
+ }),
25
+ useOrganizationMembers: (id: string) =>
26
+ useQuery({
27
+ queryKey: [...organizationQueryKey(id), 'members'],
28
+ queryFn: () => client.organizations.members(id),
29
+ enabled: id.length > 0,
30
+ }),
31
+ useOrganizationInvites: (id: string, enabled = true) =>
32
+ useQuery({
33
+ queryKey: [...organizationQueryKey(id), 'invites'],
34
+ queryFn: () => client.organizations.invites(id),
35
+ enabled: id.length > 0 && enabled,
36
+ retry: false,
37
+ }),
38
+ useCreateOrganization: () => {
39
+ const qc = useQueryClient();
40
+ return useMutation({
41
+ mutationFn: (body: CreateOrganizationCommand) => client.organizations.create(body),
42
+ onSuccess: () => {
43
+ void qc.invalidateQueries({ queryKey: organizationsQueryKey });
44
+ },
45
+ });
46
+ },
47
+ useInviteMember: (organizationId: string) => {
48
+ const qc = useQueryClient();
49
+ return useMutation({
50
+ mutationFn: (body: InviteMemberCommand) =>
51
+ client.organizations.invite(organizationId, body),
52
+ onSuccess: () => {
53
+ void qc.invalidateQueries({ queryKey: organizationQueryKey(organizationId) });
54
+ },
55
+ });
56
+ },
57
+ useRemoveMember: (organizationId: string) => {
58
+ const qc = useQueryClient();
59
+ return useMutation({
60
+ mutationFn: (userId: string) => client.organizations.removeMember(organizationId, userId),
61
+ onSuccess: () => {
62
+ void qc.invalidateQueries({ queryKey: organizationQueryKey(organizationId) });
63
+ },
64
+ });
65
+ },
66
+ useLeaveOrganization: () => {
67
+ const qc = useQueryClient();
68
+ return useMutation({
69
+ mutationFn: (organizationId: string) => client.organizations.leave(organizationId),
70
+ onSuccess: () => {
71
+ void qc.invalidateQueries({ queryKey: organizationsQueryKey });
72
+ },
73
+ });
74
+ },
75
+ useAcceptInvite: () =>
76
+ useMutation({
77
+ mutationFn: (body: AcceptInviteCommand) => client.organizations.acceptInvite(body),
78
+ }),
79
+ };
80
+ }
@@ -0,0 +1,32 @@
1
+ import { initContract } from '@ts-rest/core';
2
+ import { z } from 'zod';
3
+ import {
4
+ Create{{pascal}}CommandSchema,
5
+ Paginated{{pascal}}Schema,
6
+ {{pascal}}DtoSchema,
7
+ } from '../dto/{{kebab}}';
8
+ import { PageQuerySchema } from '../dto/user';
9
+ import { ErrSchema, OkSchema } from '../errors/envelope';
10
+
11
+ const c = initContract();
12
+
13
+ export const {{camel}}Contract = c.router({
14
+ list: {
15
+ method: 'GET',
16
+ path: '/v1/{{kebab}}',
17
+ query: PageQuerySchema,
18
+ responses: { 200: OkSchema(Paginated{{pascal}}Schema), 401: ErrSchema },
19
+ summary: 'List my {{kebab}}',
20
+ },
21
+ create: {
22
+ method: 'POST',
23
+ path: '/v1/{{kebab}}',
24
+ body: Create{{pascal}}CommandSchema,
25
+ responses: {
26
+ 201: OkSchema({{pascal}}DtoSchema),
27
+ 401: ErrSchema,
28
+ 422: ErrSchema,
29
+ },
30
+ summary: 'Create a {{kebab}} row',
31
+ },
32
+ });
@@ -0,0 +1,9 @@
1
+ import type { Create{{pascal}}Command, PageQuery } from '@ysk-kit/contracts';
2
+ import type { I{{pascal}}Repository } from '../domain/{{kebab}}-repository';
3
+
4
+ export const create{{pascal}}Service = (repo: I{{pascal}}Repository) => ({
5
+ list: (authorId: string, query: PageQuery) => repo.listForAuthor(authorId, query),
6
+ create: (authorId: string, input: Create{{pascal}}Command) => repo.create(authorId, input),
7
+ });
8
+
9
+ export type {{pascal}}Service = ReturnType<typeof create{{pascal}}Service>;
@@ -0,0 +1,15 @@
1
+ import type { Create{{pascal}}Command, PageQuery, Paginated{{pascal}}, {{pascal}}Dto } from '@ysk-kit/contracts';
2
+
3
+ export type {{pascal}}Record = {
4
+ id: string;
5
+ title: string;
6
+ body: string;
7
+ authorId: string;
8
+ createdAt: Date;
9
+ updatedAt: Date;
10
+ };
11
+
12
+ export interface I{{pascal}}Repository {
13
+ create(authorId: string, input: Create{{pascal}}Command): Promise<{{pascal}}Dto>;
14
+ listForAuthor(authorId: string, query: PageQuery): Promise<Paginated{{pascal}}>;
15
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { PaginatedSchema } from './user';
3
+
4
+ export const {{pascal}}DtoSchema = z.object({
5
+ id: z.string().uuid(),
6
+ title: z.string().min(1).max(200),
7
+ body: z.string().max(8000),
8
+ authorId: z.string().uuid(),
9
+ createdAt: z.iso.datetime(),
10
+ updatedAt: z.iso.datetime(),
11
+ });
12
+ export type {{pascal}}Dto = z.infer<typeof {{pascal}}DtoSchema>;
13
+
14
+ export const Paginated{{pascal}}Schema = PaginatedSchema({{pascal}}DtoSchema);
15
+ export type Paginated{{pascal}} = z.infer<typeof Paginated{{pascal}}Schema>;
16
+
17
+ export const Create{{pascal}}CommandSchema = z.object({
18
+ title: z.string().min(1).max(200),
19
+ body: z.string().max(8000).default(''),
20
+ });
21
+ export type Create{{pascal}}Command = z.infer<typeof Create{{pascal}}CommandSchema>;
@@ -0,0 +1,36 @@
1
+ import { slicePage } from '@ysk-kit/application';
2
+ import type { {{pascal}}Dto } from '@ysk-kit/contracts';
3
+ import type { I{{pascal}}Repository, {{pascal}}Record } from '../domain/{{kebab}}-repository';
4
+
5
+ const toDto = (row: {{pascal}}Record): {{pascal}}Dto => ({
6
+ id: row.id,
7
+ title: row.title,
8
+ body: row.body,
9
+ authorId: row.authorId,
10
+ createdAt: row.createdAt.toISOString(),
11
+ updatedAt: row.updatedAt.toISOString(),
12
+ });
13
+
14
+ export const createMemory{{pascal}}Repository = (): I{{pascal}}Repository => {
15
+ const rows: {{pascal}}Record[] = [];
16
+ return {
17
+ async create(authorId, input) {
18
+ const now = new Date();
19
+ const row: {{pascal}}Record = {
20
+ id: crypto.randomUUID(),
21
+ title: input.title,
22
+ body: input.body,
23
+ authorId,
24
+ createdAt: now,
25
+ updatedAt: now,
26
+ };
27
+ rows.unshift(row);
28
+ return toDto(row);
29
+ },
30
+ async listForAuthor(authorId, query) {
31
+ const mine = rows.filter((row) => row.authorId === authorId);
32
+ const page = slicePage(mine, query.limit);
33
+ return { items: page.items.map(toDto), nextCursor: page.nextCursor };
34
+ },
35
+ };
36
+ };
@@ -0,0 +1,8 @@
1
+ model {{pascal}} {
2
+ id String @id @default(uuid())
3
+ title String
4
+ body String @default("")
5
+ authorId String
6
+ createdAt DateTime @default(now())
7
+ updatedAt DateTime @updatedAt
8
+ }