@tearleads/client-sdk 0.1.86 → 0.1.87
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/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import type { ExecSql } from "../../../data/sqlite/sqlSchema.js";
|
|
2
2
|
/**
|
|
3
|
-
* A locally-known organization the user
|
|
4
|
-
*
|
|
3
|
+
* A locally-known organization: one the user holds at least one persisted
|
|
4
|
+
* container of. `name` is the decrypted organization-profile display name when
|
|
5
5
|
* one is stored locally, or null when it has not synced yet.
|
|
6
|
+
*
|
|
7
|
+
* `rootContainerId` is the organization's workspace root when this user can
|
|
8
|
+
* reach it. It is null for an organization the user reaches only through its
|
|
9
|
+
* Members-granted metadata container or through folders shared from beneath a
|
|
10
|
+
* root the user cannot see; such an organization can be browsed but not made
|
|
11
|
+
* the active workspace.
|
|
6
12
|
*/
|
|
7
13
|
export interface LocalOrganizationSummary {
|
|
8
14
|
name: string | null;
|
|
9
15
|
organizationId: string;
|
|
10
|
-
rootContainerId: string;
|
|
16
|
+
rootContainerId: string | null;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
13
|
-
* Enumerates
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Enumerates every organization the user holds a persisted container of, from
|
|
20
|
+
* local state alone. This is intentionally offline: membership is visible
|
|
21
|
+
* locally because every Members recipient can read the organization's metadata
|
|
22
|
+
* root, and shared folders arrive with their organization attached, so listing
|
|
23
|
+
* organizations needs no server round-trip.
|
|
17
24
|
*/
|
|
18
25
|
export declare function listLocalOrganizations(input: {
|
|
19
26
|
execSql: ExecSql;
|
|
@@ -84,54 +84,74 @@ async function readLocalOrganizationName(execSql, organizationId, organizationCo
|
|
|
84
84
|
}
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
|
+
// Groups every persisted container by organization, keeping the first
|
|
88
|
+
// workspace root (a parentless, non-system container) and the earliest local
|
|
89
|
+
// creation time. Name resolution reuses the grouped containers to locate an
|
|
90
|
+
// organization's independent metadata root without a second DB scan.
|
|
91
|
+
function indexLocalOrganizations(containers) {
|
|
92
|
+
const organizations = new Map();
|
|
93
|
+
for (const { container } of containers) {
|
|
94
|
+
const localCreatedAt = container.localCreatedAt ?? "";
|
|
95
|
+
let organization = organizations.get(container.organizationId);
|
|
96
|
+
if (!organization) {
|
|
97
|
+
organization = {
|
|
98
|
+
containers: [],
|
|
99
|
+
earliestLocalCreatedAt: localCreatedAt,
|
|
100
|
+
root: null,
|
|
101
|
+
};
|
|
102
|
+
organizations.set(container.organizationId, organization);
|
|
103
|
+
}
|
|
104
|
+
organization.containers.push({
|
|
105
|
+
id: container.id,
|
|
106
|
+
systemSlot: container.systemSlot ?? null,
|
|
107
|
+
});
|
|
108
|
+
if (localCreatedAt < organization.earliestLocalCreatedAt) {
|
|
109
|
+
organization.earliestLocalCreatedAt = localCreatedAt;
|
|
110
|
+
}
|
|
111
|
+
if (organization.root === null &&
|
|
112
|
+
container.parentId === null &&
|
|
113
|
+
!container.systemSlot) {
|
|
114
|
+
organization.root = { id: container.id, localCreatedAt };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return organizations;
|
|
118
|
+
}
|
|
119
|
+
// Workspaces (organizations with a reachable root) come first in root
|
|
120
|
+
// creation order, so the personal organization provisioned with this device
|
|
121
|
+
// keeps its place. Organizations reached without a root follow in the order
|
|
122
|
+
// their first container arrived locally.
|
|
123
|
+
function compareLocalOrganizations([leftId, left], [rightId, right]) {
|
|
124
|
+
if (left.root && right.root) {
|
|
125
|
+
return (left.root.localCreatedAt.localeCompare(right.root.localCreatedAt) ||
|
|
126
|
+
leftId.localeCompare(rightId) ||
|
|
127
|
+
left.root.id.localeCompare(right.root.id));
|
|
128
|
+
}
|
|
129
|
+
if (left.root || right.root) {
|
|
130
|
+
return left.root ? -1 : 1;
|
|
131
|
+
}
|
|
132
|
+
return (left.earliestLocalCreatedAt.localeCompare(right.earliestLocalCreatedAt) ||
|
|
133
|
+
leftId.localeCompare(rightId));
|
|
134
|
+
}
|
|
87
135
|
/**
|
|
88
|
-
* Enumerates
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
136
|
+
* Enumerates every organization the user holds a persisted container of, from
|
|
137
|
+
* local state alone. This is intentionally offline: membership is visible
|
|
138
|
+
* locally because every Members recipient can read the organization's metadata
|
|
139
|
+
* root, and shared folders arrive with their organization attached, so listing
|
|
140
|
+
* organizations needs no server round-trip.
|
|
92
141
|
*/
|
|
93
142
|
export async function listLocalOrganizations(input) {
|
|
94
143
|
const containers = await sqlContainerContentsPersistence.loadContainers(input.execSql);
|
|
95
144
|
// A projected read model is not a precondition for listing organizations, so
|
|
96
145
|
// an unreadable one degrades to the container-derived lookups below.
|
|
97
146
|
const profileDocumentIdsByOrganizationId = await loadOrganizationProfileDocumentIds(input.execSql).catch(() => new Map());
|
|
98
|
-
|
|
99
|
-
// org's independent metadata root without a second DB scan.
|
|
100
|
-
const containersByOrganizationId = new Map();
|
|
101
|
-
for (const { container } of containers) {
|
|
102
|
-
const entry = {
|
|
103
|
-
id: container.id,
|
|
104
|
-
systemSlot: container.systemSlot ?? null,
|
|
105
|
-
};
|
|
106
|
-
const group = containersByOrganizationId.get(container.organizationId);
|
|
107
|
-
if (group) {
|
|
108
|
-
group.push(entry);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
containersByOrganizationId.set(container.organizationId, [entry]);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const seen = new Set();
|
|
115
|
-
const rootContainers = containers.filter(({ container }) => {
|
|
116
|
-
if (container.parentId !== null ||
|
|
117
|
-
container.systemSlot ||
|
|
118
|
-
seen.has(container.organizationId)) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
seen.add(container.organizationId);
|
|
122
|
-
return true;
|
|
123
|
-
});
|
|
124
|
-
rootContainers.sort((left, right) => (left.container.localCreatedAt ?? "").localeCompare(right.container.localCreatedAt ?? "") ||
|
|
125
|
-
left.container.organizationId.localeCompare(right.container.organizationId) ||
|
|
126
|
-
left.container.id.localeCompare(right.container.id));
|
|
147
|
+
const organizations = [...indexLocalOrganizations(containers)].sort(compareLocalOrganizations);
|
|
127
148
|
// Read each organization's profile name concurrently; readLocalOrganizationName
|
|
128
149
|
// already falls back to null per organization, so a single corrupt profile
|
|
129
150
|
// cannot reject the batch.
|
|
130
|
-
return Promise.all(
|
|
131
|
-
name: await readLocalOrganizationName(input.execSql,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
rootContainerId: container.id,
|
|
151
|
+
return Promise.all(organizations.map(async ([organizationId, organization]) => ({
|
|
152
|
+
name: await readLocalOrganizationName(input.execSql, organizationId, organization.containers, profileDocumentIdsByOrganizationId.get(organizationId) ?? null),
|
|
153
|
+
organizationId,
|
|
154
|
+
rootContainerId: organization.root?.id ?? null,
|
|
135
155
|
})));
|
|
136
156
|
}
|
|
137
157
|
//# sourceMappingURL=listLocalOrganizations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listLocalOrganizations.js","sourceRoot":"","sources":["listLocalOrganizations.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredDocumentState } from \"../../../data/documents/documentKinds\";\nimport { sqlContainerContentsPersistence } from \"../../../data/persistence/container-contents/containerContentsPersistence\";\nimport {\n DOCUMENTS_APP_KIND,\n sqlDocumentsPersistence,\n} from \"../../../data/persistence/documents/documentsPersistence\";\nimport { findLocalIdByDocumentId } from \"../../../data/persistence/documents/records\";\nimport { loadOrganizationProfileDocumentIds } from \"../../../data/persistence/organizations/organizationProfileDocumentPointers\";\nimport type { ExecSql } from \"../../../data/sqlite/sqlSchema\";\nimport { loadPersistedDocumentContent } from \"../../documents\";\nimport {\n getOrganizationProfileDocumentLocalId,\n readOrganizationProfileName,\n} from \"../organizationProfile\";\nimport { deriveOrganizationMetadataContainerSystemSlot } from \"../rosterProfileContainer\";\n\n// Just the container fields this module needs to locate an org's metadata\n// container among the locally-persisted containers.\ninterface LocalContainerForNameLookup {\n id: string;\n systemSlot: string | null;\n}\n\n/**\n * A locally-known organization the user belongs to, derived from its persisted\n * root container. `name` is the decrypted organization-profile display name when\n * one is stored locally, or null when it has not synced yet.\n */\nexport interface LocalOrganizationSummary {\n name: string | null;\n organizationId: string;\n rootContainerId: string;\n}\n\n// Reads and decrypts the `name` field of the organization_profile document\n// persisted under `documentLocalId`, or null if it is absent, empty, or\n// unparseable. A single corrupt profile must not break the whole list, so every\n// failure collapses to a null (unnamed) organization.\nasync function readOrganizationNameFromDocument(\n execSql: ExecSql,\n documentLocalId: string,\n): Promise<string | null> {\n try {\n const doc = await loadPersistedDocumentContent({\n execSql,\n localId: documentLocalId,\n persistence: sqlDocumentsPersistence,\n });\n if (!doc) {\n return null;\n }\n\n return readOrganizationProfileName(\n readStoredDocumentState(doc).structuredFields,\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolves an organization's decrypted display name from local state.\n *\n * The same organization_profile document is addressed by several different\n * local-id conventions depending on how it arrived, so one lookup by id is not\n * enough:\n *\n * - The org's own **provisioner** (and any admin who later edits the name)\n * writes the document under the deterministic alias\n * `org-profile:<organizationId>` — resolved by the fast path below.\n * - A **device that only synced it** — a member of another org, or this same\n * user's freshly re-hydrated device after an identity restore — never runs\n * that write. The sync ingest keys the document under the server documentId\n * (there is no local alias to reuse — see `upsertDiscoveredDocumentWithExec`),\n * so the alias lookup misses.\n *\n * Two fallbacks cover the synced case, in order of authority:\n *\n * 1. The **organization read model** records the org's `profileDocumentId`.\n * That pointer is container-independent, so it resolves the name wherever\n * the document happens to be linked — including a profile document created\n * by an admin through the org-manager editor rather than by provisioning.\n * 2. Failing that, locate the org's metadata container by its deterministic\n * system slot and read whichever profile document is linked there. Keying on\n * the system slot (rather than the document kind) keeps it correct even\n * before the projected document kind has been rebuilt from a freshly-synced\n * snapshot, and it still works before the read model has been projected.\n */\nasync function readLocalOrganizationName(\n execSql: ExecSql,\n organizationId: string,\n organizationContainers: ReadonlyArray<LocalContainerForNameLookup>,\n profileDocumentId: string | null,\n): Promise<string | null> {\n const aliasName = await readOrganizationNameFromDocument(\n execSql,\n getOrganizationProfileDocumentLocalId({ organizationId }),\n );\n if (aliasName !== null) {\n return aliasName;\n }\n\n if (profileDocumentId) {\n // Reach the row wherever it landed: a device that only synced the document\n // keys it under the server documentId rather than any local alias.\n const syncedLocalId = await findLocalIdByDocumentId(\n execSql,\n DOCUMENTS_APP_KIND,\n profileDocumentId,\n );\n const syncedName = syncedLocalId\n ? await readOrganizationNameFromDocument(execSql, syncedLocalId)\n : null;\n if (syncedName !== null) {\n return syncedName;\n }\n }\n\n const metadataSystemSlot =\n await deriveOrganizationMetadataContainerSystemSlot({ organizationId });\n const metadataContainer = organizationContainers.find(\n (container) => container.systemSlot === metadataSystemSlot,\n );\n if (!metadataContainer) {\n return null;\n }\n\n const documentLocalIds =\n await sqlDocumentsPersistence.findDocumentLocalIdsByContainerId(\n execSql,\n metadataContainer.id,\n );\n for (const documentLocalId of documentLocalIds) {\n const name = await readOrganizationNameFromDocument(\n execSql,\n documentLocalId,\n );\n if (name !== null) {\n return name;\n }\n }\n return null;\n}\n\n/**\n * Enumerates the organizations the user can manage from local state: one per\n * persisted root container. This is intentionally offline — the Explorer already\n * resolves files across organizations by effective permission, so listing the\n * organizations to manage needs no server round-trip.\n */\nexport async function listLocalOrganizations(input: {\n execSql: ExecSql;\n}): Promise<LocalOrganizationSummary[]> {\n const containers = await sqlContainerContentsPersistence.loadContainers(\n input.execSql,\n );\n // A projected read model is not a precondition for listing organizations, so\n // an unreadable one degrades to the container-derived lookups below.\n const profileDocumentIdsByOrganizationId =\n await loadOrganizationProfileDocumentIds(input.execSql).catch(\n () => new Map<string, string>(),\n );\n\n // Group every persisted container by org so name resolution can locate an\n // org's independent metadata root without a second DB scan.\n const containersByOrganizationId = new Map<\n string,\n LocalContainerForNameLookup[]\n >();\n for (const { container } of containers) {\n const entry: LocalContainerForNameLookup = {\n id: container.id,\n systemSlot: container.systemSlot ?? null,\n };\n const group = containersByOrganizationId.get(container.organizationId);\n if (group) {\n group.push(entry);\n } else {\n containersByOrganizationId.set(container.organizationId, [entry]);\n }\n }\n\n const seen = new Set<string>();\n const rootContainers = containers.filter(({ container }) => {\n if (\n container.parentId !== null ||\n container.systemSlot ||\n seen.has(container.organizationId)\n ) {\n return false;\n }\n seen.add(container.organizationId);\n return true;\n });\n rootContainers.sort(\n (left, right) =>\n (left.container.localCreatedAt ?? \"\").localeCompare(\n right.container.localCreatedAt ?? \"\",\n ) ||\n left.container.organizationId.localeCompare(\n right.container.organizationId,\n ) ||\n left.container.id.localeCompare(right.container.id),\n );\n\n // Read each organization's profile name concurrently; readLocalOrganizationName\n // already falls back to null per organization, so a single corrupt profile\n // cannot reject the batch.\n return Promise.all(\n rootContainers.map(async ({ container }) => ({\n name: await readLocalOrganizationName(\n input.execSql,\n container.organizationId,\n containersByOrganizationId.get(container.organizationId) ?? [],\n profileDocumentIdsByOrganizationId.get(container.organizationId) ??\n null,\n ),\n organizationId: container.organizationId,\n rootContainerId: container.id,\n })),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"listLocalOrganizations.js","sourceRoot":"","sources":["listLocalOrganizations.ts"],"names":[],"mappings":"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","sourcesContent":["import { readStoredDocumentState } from \"../../../data/documents/documentKinds\";\nimport { sqlContainerContentsPersistence } from \"../../../data/persistence/container-contents/containerContentsPersistence\";\nimport {\n DOCUMENTS_APP_KIND,\n sqlDocumentsPersistence,\n} from \"../../../data/persistence/documents/documentsPersistence\";\nimport { findLocalIdByDocumentId } from \"../../../data/persistence/documents/records\";\nimport { loadOrganizationProfileDocumentIds } from \"../../../data/persistence/organizations/organizationProfileDocumentPointers\";\nimport type { ExecSql } from \"../../../data/sqlite/sqlSchema\";\nimport { loadPersistedDocumentContent } from \"../../documents\";\nimport {\n getOrganizationProfileDocumentLocalId,\n readOrganizationProfileName,\n} from \"../organizationProfile\";\nimport { deriveOrganizationMetadataContainerSystemSlot } from \"../rosterProfileContainer\";\n\n// Just the container fields this module needs to locate an org's metadata\n// container among the locally-persisted containers.\ninterface LocalContainerForNameLookup {\n id: string;\n systemSlot: string | null;\n}\n\n/**\n * A locally-known organization: one the user holds at least one persisted\n * container of. `name` is the decrypted organization-profile display name when\n * one is stored locally, or null when it has not synced yet.\n *\n * `rootContainerId` is the organization's workspace root when this user can\n * reach it. It is null for an organization the user reaches only through its\n * Members-granted metadata container or through folders shared from beneath a\n * root the user cannot see; such an organization can be browsed but not made\n * the active workspace.\n */\nexport interface LocalOrganizationSummary {\n name: string | null;\n organizationId: string;\n rootContainerId: string | null;\n}\n\n// Reads and decrypts the `name` field of the organization_profile document\n// persisted under `documentLocalId`, or null if it is absent, empty, or\n// unparseable. A single corrupt profile must not break the whole list, so every\n// failure collapses to a null (unnamed) organization.\nasync function readOrganizationNameFromDocument(\n execSql: ExecSql,\n documentLocalId: string,\n): Promise<string | null> {\n try {\n const doc = await loadPersistedDocumentContent({\n execSql,\n localId: documentLocalId,\n persistence: sqlDocumentsPersistence,\n });\n if (!doc) {\n return null;\n }\n\n return readOrganizationProfileName(\n readStoredDocumentState(doc).structuredFields,\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolves an organization's decrypted display name from local state.\n *\n * The same organization_profile document is addressed by several different\n * local-id conventions depending on how it arrived, so one lookup by id is not\n * enough:\n *\n * - The org's own **provisioner** (and any admin who later edits the name)\n * writes the document under the deterministic alias\n * `org-profile:<organizationId>` — resolved by the fast path below.\n * - A **device that only synced it** — a member of another org, or this same\n * user's freshly re-hydrated device after an identity restore — never runs\n * that write. The sync ingest keys the document under the server documentId\n * (there is no local alias to reuse — see `upsertDiscoveredDocumentWithExec`),\n * so the alias lookup misses.\n *\n * Two fallbacks cover the synced case, in order of authority:\n *\n * 1. The **organization read model** records the org's `profileDocumentId`.\n * That pointer is container-independent, so it resolves the name wherever\n * the document happens to be linked — including a profile document created\n * by an admin through the org-manager editor rather than by provisioning.\n * 2. Failing that, locate the org's metadata container by its deterministic\n * system slot and read whichever profile document is linked there. Keying on\n * the system slot (rather than the document kind) keeps it correct even\n * before the projected document kind has been rebuilt from a freshly-synced\n * snapshot, and it still works before the read model has been projected.\n */\nasync function readLocalOrganizationName(\n execSql: ExecSql,\n organizationId: string,\n organizationContainers: ReadonlyArray<LocalContainerForNameLookup>,\n profileDocumentId: string | null,\n): Promise<string | null> {\n const aliasName = await readOrganizationNameFromDocument(\n execSql,\n getOrganizationProfileDocumentLocalId({ organizationId }),\n );\n if (aliasName !== null) {\n return aliasName;\n }\n\n if (profileDocumentId) {\n // Reach the row wherever it landed: a device that only synced the document\n // keys it under the server documentId rather than any local alias.\n const syncedLocalId = await findLocalIdByDocumentId(\n execSql,\n DOCUMENTS_APP_KIND,\n profileDocumentId,\n );\n const syncedName = syncedLocalId\n ? await readOrganizationNameFromDocument(execSql, syncedLocalId)\n : null;\n if (syncedName !== null) {\n return syncedName;\n }\n }\n\n const metadataSystemSlot =\n await deriveOrganizationMetadataContainerSystemSlot({ organizationId });\n const metadataContainer = organizationContainers.find(\n (container) => container.systemSlot === metadataSystemSlot,\n );\n if (!metadataContainer) {\n return null;\n }\n\n const documentLocalIds =\n await sqlDocumentsPersistence.findDocumentLocalIdsByContainerId(\n execSql,\n metadataContainer.id,\n );\n for (const documentLocalId of documentLocalIds) {\n const name = await readOrganizationNameFromDocument(\n execSql,\n documentLocalId,\n );\n if (name !== null) {\n return name;\n }\n }\n return null;\n}\n\ninterface LocalOrganizationContainers {\n readonly containers: LocalContainerForNameLookup[];\n earliestLocalCreatedAt: string;\n root: { readonly id: string; readonly localCreatedAt: string } | null;\n}\n\ntype PersistedContainers = Awaited<\n ReturnType<typeof sqlContainerContentsPersistence.loadContainers>\n>;\n\n// Groups every persisted container by organization, keeping the first\n// workspace root (a parentless, non-system container) and the earliest local\n// creation time. Name resolution reuses the grouped containers to locate an\n// organization's independent metadata root without a second DB scan.\nfunction indexLocalOrganizations(\n containers: PersistedContainers,\n): Map<string, LocalOrganizationContainers> {\n const organizations = new Map<string, LocalOrganizationContainers>();\n for (const { container } of containers) {\n const localCreatedAt = container.localCreatedAt ?? \"\";\n let organization = organizations.get(container.organizationId);\n if (!organization) {\n organization = {\n containers: [],\n earliestLocalCreatedAt: localCreatedAt,\n root: null,\n };\n organizations.set(container.organizationId, organization);\n }\n organization.containers.push({\n id: container.id,\n systemSlot: container.systemSlot ?? null,\n });\n if (localCreatedAt < organization.earliestLocalCreatedAt) {\n organization.earliestLocalCreatedAt = localCreatedAt;\n }\n if (\n organization.root === null &&\n container.parentId === null &&\n !container.systemSlot\n ) {\n organization.root = { id: container.id, localCreatedAt };\n }\n }\n return organizations;\n}\n\n// Workspaces (organizations with a reachable root) come first in root\n// creation order, so the personal organization provisioned with this device\n// keeps its place. Organizations reached without a root follow in the order\n// their first container arrived locally.\nfunction compareLocalOrganizations(\n [leftId, left]: [string, LocalOrganizationContainers],\n [rightId, right]: [string, LocalOrganizationContainers],\n): number {\n if (left.root && right.root) {\n return (\n left.root.localCreatedAt.localeCompare(right.root.localCreatedAt) ||\n leftId.localeCompare(rightId) ||\n left.root.id.localeCompare(right.root.id)\n );\n }\n if (left.root || right.root) {\n return left.root ? -1 : 1;\n }\n return (\n left.earliestLocalCreatedAt.localeCompare(right.earliestLocalCreatedAt) ||\n leftId.localeCompare(rightId)\n );\n}\n\n/**\n * Enumerates every organization the user holds a persisted container of, from\n * local state alone. This is intentionally offline: membership is visible\n * locally because every Members recipient can read the organization's metadata\n * root, and shared folders arrive with their organization attached, so listing\n * organizations needs no server round-trip.\n */\nexport async function listLocalOrganizations(input: {\n execSql: ExecSql;\n}): Promise<LocalOrganizationSummary[]> {\n const containers = await sqlContainerContentsPersistence.loadContainers(\n input.execSql,\n );\n // A projected read model is not a precondition for listing organizations, so\n // an unreadable one degrades to the container-derived lookups below.\n const profileDocumentIdsByOrganizationId =\n await loadOrganizationProfileDocumentIds(input.execSql).catch(\n () => new Map<string, string>(),\n );\n const organizations = [...indexLocalOrganizations(containers)].sort(\n compareLocalOrganizations,\n );\n\n // Read each organization's profile name concurrently; readLocalOrganizationName\n // already falls back to null per organization, so a single corrupt profile\n // cannot reject the batch.\n return Promise.all(\n organizations.map(async ([organizationId, organization]) => ({\n name: await readLocalOrganizationName(\n input.execSql,\n organizationId,\n organization.containers,\n profileDocumentIdsByOrganizationId.get(organizationId) ?? null,\n ),\n organizationId,\n rootContainerId: organization.root?.id ?? null,\n })),\n );\n}\n"]}
|