@assembly-js/app-foundation 0.14.3 → 0.14.4

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.
package/CHANGELOG.md CHANGED
@@ -19,6 +19,9 @@ A patch release never changes what an app's build requires; a minor release may
19
19
  not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
20
20
  `bump: minor` change file.
21
21
 
22
+ ## 0.14.4
23
+ - [guidance] A selected company now gets one message on its company channel instead of a new individual channel per client.
24
+
22
25
  ## 0.14.3
23
26
  - [guidance] The build agent now sets up services that sign in with OAuth (such as Dropbox) with a connection that renews itself, explains every setup step up front, and never asks for a token in chat.
24
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -69,6 +69,41 @@ await assembly.sendMessage({
69
69
 
70
70
  `retrieveMessageChannel({ id })` is for a channel id you already resolved this request — never for one typed into the source.
71
71
 
72
+ ## A company is one channel
73
+
74
+ When the user selects a **company**, send one message on that company's channel. Do not expand the company into its clients, and do not create an individual channel per client. That fan-out opens a new 1:1 thread for every person in the company and delivers the message there instead of the company thread they asked for.
75
+
76
+ Individual channels are only for a recipient the user picked as a specific client. The same rule applies to file channels: a company attachment goes on the company file channel.
77
+
78
+ ```ts
79
+ async function findOrCreateCompanyMessageChannel(
80
+ assembly: AssemblyAPI,
81
+ companyId: string,
82
+ ): Promise<string> {
83
+ const { data } = await assembly.listMessageChannels({
84
+ companyId,
85
+ membershipType: 'company',
86
+ limit: 1,
87
+ });
88
+ // A 1:1 thread for someone at this company also carries companyId. Send only
89
+ // when the row is the company channel.
90
+ const existing = data?.find((channel) => channel.membershipType === 'company');
91
+ if (existing?.id) return existing.id;
92
+
93
+ // membershipType is a string enum the package root does not re-export, so a
94
+ // bare 'company' literal does not typecheck. Assert the request body.
95
+ const created = await assembly.createMessageChannel({
96
+ requestBody: { companyId, membershipType: 'company' } as Parameters<
97
+ AssemblyAPI['createMessageChannel']
98
+ >[0]['requestBody'],
99
+ });
100
+ if (!created.id) throw new Error('Company channel was not created');
101
+ return created.id;
102
+ }
103
+ ```
104
+
105
+ Pass `companyId` when creating an individual channel for a client who belongs to more than one company. A create with only `clientId` is rejected for those clients.
106
+
72
107
  ## Discovering a file channel
73
108
 
74
109
  ```ts
@@ -113,6 +148,7 @@ Render an explicit, honest empty state and stop — "The intake form hasn't been
113
148
  | Matched on the id instead of a stable attribute | Same — an id is not stable across workspaces, a name is |
114
149
  | Matched a form on `form.name` | Always undefined; the display name is `form.fields.name`, so nothing ever matches and the app renders its empty state forever |
115
150
  | Fell back to a literal id when discovery found nothing | The feature appears to work and writes into the wrong place, or fails on a resource the workspace can't see |
151
+ | Expanded a selected company into one individual channel per client | The workspace gets a burst of new 1:1 threads, and the message never lands in the company channel |
116
152
  | Used the Files module for app-private uploads | Attachments show up in the client's portal Files tab |
117
153
 
118
154
  ## Edge cases
@@ -121,5 +157,5 @@ Render an explicit, honest empty state and stop — "The intake form hasn't been
121
157
  - **Every list method takes an options object**, even when you pass no filters — `listForms({})`, not `listForms()`.
122
158
  - **List responses are paginated envelopes**, and `data` is nullable: destructure with a default and still index defensively (`const { data: forms = [] } = …; forms?.[0]`).
123
159
  - **Narrow before you index.** Taking `[0]` off an unfiltered list is the same guess as a hardcoded id. Filter by `membershipType` (and `companyId` for a client in multiple companies); if two candidates genuinely remain, surface the choice rather than picking one.
124
- - **Creating a channel is not a substitute for finding one.** `createMessageChannel` requires a `membershipType`, and its type is a string enum the SDK does not re-export from its package root — you cannot satisfy it with a plain `'individual'` string. Work with the channels the workspace already has; check the SDK types first if you genuinely need to create one.
160
+ - **Look up the channel, then create it only if that company or client does not have one yet.** Use `membershipType: 'company'` with `companyId` for a company, and `membershipType: 'individual'` with `clientId` (plus `companyId` when the client is in multiple companies) for one client. The enum is not re-exported from the package root, so assert the request body as in the company example. Never turn a company selection into one individual channel per member.
125
161
  - **Ids from the app bridge or a route param** are runtime values, not literals — those are fine to pass straight to `retrieve*`, after the usual auth and scope checks (see `assembly-auth`).