@peers-app/peers-sdk 0.23.2 → 0.23.5
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/dist/context/data-context.d.ts +36 -5
- package/dist/context/data-context.js +44 -9
- package/dist/context/user-context.d.ts +44 -0
- package/dist/context/user-context.js +91 -12
- package/dist/contracts/__tests__/contract-provider-router.test.js +26 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +44 -0
- package/dist/contracts/__tests__/registry.test.js +104 -0
- package/dist/contracts/contract-provider-router.d.ts +21 -0
- package/dist/contracts/contract-provider-router.js +31 -5
- package/dist/contracts/contract-proxy.d.ts +3 -0
- package/dist/contracts/contract-proxy.js +3 -0
- package/dist/contracts/contract-resolvers.d.ts +21 -3
- package/dist/contracts/contract-resolvers.js +105 -32
- package/dist/contracts/index.d.ts +3 -3
- package/dist/contracts/registry.d.ts +59 -0
- package/dist/contracts/registry.js +155 -25
- package/dist/contracts/system-contracts.js +3 -1
- package/dist/data/group-share.d.ts +13 -0
- package/dist/data/group-share.js +32 -8
- package/dist/data/group-share.test.d.ts +1 -0
- package/dist/data/group-share.test.js +110 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/types.d.ts +19 -0
- package/dist/data/orm/types.js +11 -0
- package/dist/data/package-table-identity.d.ts +46 -0
- package/dist/data/package-table-identity.js +71 -0
- package/dist/data/package-table-identity.test.d.ts +1 -0
- package/dist/data/package-table-identity.test.js +46 -0
- package/dist/data/package-version-resolver.d.ts +13 -0
- package/dist/data/package-version-resolver.js +142 -6
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/persistent-var-identity.d.ts +37 -0
- package/dist/data/persistent-var-identity.js +69 -0
- package/dist/data/persistent-vars.d.ts +3 -5
- package/dist/data/persistent-vars.js +25 -13
- package/dist/data/persistent-vars.package-scope.test.d.ts +1 -0
- package/dist/data/persistent-vars.package-scope.test.js +27 -0
- package/dist/data/tools.d.ts +3 -3
- package/dist/group-invite/group-invite.types.d.ts +15 -5
- package/dist/package-loader/contract-package-loader.js +20 -8
- package/dist/package-loader/contract-package-loader.test.js +16 -0
- package/dist/package-loader/index.d.ts +4 -0
- package/dist/package-loader/index.js +4 -0
- package/dist/package-loader/isolated-artifact.d.ts +233 -0
- package/dist/package-loader/isolated-artifact.js +643 -0
- package/dist/package-loader/isolated-artifact.test.d.ts +1 -0
- package/dist/package-loader/isolated-artifact.test.js +270 -0
- package/dist/package-loader/isolated-persistent-var-gateway.d.ts +20 -0
- package/dist/package-loader/isolated-persistent-var-gateway.js +106 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-persistent-var-gateway.test.js +56 -0
- package/dist/package-loader/isolated-runtime.d.ts +229 -0
- package/dist/package-loader/isolated-runtime.js +37 -0
- package/dist/package-loader/isolated-table-gateway.d.ts +16 -0
- package/dist/package-loader/isolated-table-gateway.js +375 -0
- package/dist/package-loader/isolated-table-gateway.test.d.ts +1 -0
- package/dist/package-loader/isolated-table-gateway.test.js +206 -0
- package/dist/package-loader/isolated-value.d.ts +14 -0
- package/dist/package-loader/isolated-value.js +63 -0
- package/dist/package-loader/package-loader.d.ts +123 -4
- package/dist/package-loader/package-loader.js +417 -46
- package/dist/package-loader/package-loader.test.js +478 -12
- package/dist/package-loader/resolve-consumed-contracts.d.ts +3 -3
- package/dist/package-loader/resolve-consumed-contracts.js +18 -10
- package/dist/utils.d.ts +21 -2
- package/dist/utils.js +46 -7
- package/package.json +1 -1
|
@@ -31,26 +31,139 @@ class ContractRegistry {
|
|
|
31
31
|
* @returns Validation result. If invalid the provider is not registered.
|
|
32
32
|
*/
|
|
33
33
|
register(providerPackageId, definition, alsoImplementsDecls = []) {
|
|
34
|
+
return this.registerBatch(providerPackageId, [
|
|
35
|
+
{ definition, alsoImplements: alsoImplementsDecls },
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Atomically register every contract supplied by one provider package.
|
|
40
|
+
*
|
|
41
|
+
* Frozen-shape and `alsoImplements` checks run against a candidate registry
|
|
42
|
+
* containing the whole batch, so declarations may reference another
|
|
43
|
+
* contract in the same install. If any entry fails, no definition, provider,
|
|
44
|
+
* active-provider pointer, or reverse index is changed.
|
|
45
|
+
*
|
|
46
|
+
* @param providerPackageId - Trusted identity of the provider package.
|
|
47
|
+
* @param registrations - Contract definitions and compatibility declarations.
|
|
48
|
+
* @returns The combined validation result.
|
|
49
|
+
*/
|
|
50
|
+
registerBatch(providerPackageId, registrations) {
|
|
51
|
+
const validation = this.validateBatch(providerPackageId, registrations);
|
|
52
|
+
if (!validation.valid) {
|
|
53
|
+
return validation;
|
|
54
|
+
}
|
|
55
|
+
this.commitRegistrations(providerPackageId, registrations);
|
|
56
|
+
return validation;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Atomically replace the complete contract set for one provider package.
|
|
60
|
+
*
|
|
61
|
+
* Validation runs before the existing provider entries are touched. Active
|
|
62
|
+
* selections remain on this package for contract versions it still provides;
|
|
63
|
+
* removed versions fall back according to ordinary unregister semantics.
|
|
64
|
+
*
|
|
65
|
+
* @param providerPackageId - Trusted provider package identity.
|
|
66
|
+
* @param registrations - Complete replacement set.
|
|
67
|
+
* @returns Validation result; invalid replacements leave the registry unchanged.
|
|
68
|
+
*/
|
|
69
|
+
replaceBatch(providerPackageId, registrations) {
|
|
70
|
+
const validation = this.validateBatch(providerPackageId, registrations);
|
|
71
|
+
if (!validation.valid) {
|
|
72
|
+
return validation;
|
|
73
|
+
}
|
|
74
|
+
const previouslyActive = new Set((this.packageContracts.get(providerPackageId) ?? []).filter((key) => this.activeProviders.get(key)?.providerPackageId === providerPackageId));
|
|
75
|
+
this.unregister(providerPackageId);
|
|
76
|
+
this.commitRegistrations(providerPackageId, registrations);
|
|
77
|
+
for (const key of previouslyActive) {
|
|
78
|
+
const provider = this.allProviders
|
|
79
|
+
.get(key)
|
|
80
|
+
?.find((item) => item.providerPackageId === providerPackageId);
|
|
81
|
+
if (provider) {
|
|
82
|
+
this.activeProviders.set(key, provider);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return validation;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate a complete provider registration batch without changing registry state.
|
|
89
|
+
*
|
|
90
|
+
* @param providerPackageId - Trusted provider package identity.
|
|
91
|
+
* @param registrations - Definitions and compatibility declarations to validate.
|
|
92
|
+
* @returns Combined validation result.
|
|
93
|
+
*/
|
|
94
|
+
validateBatch(providerPackageId, registrations) {
|
|
95
|
+
const candidateDefinitions = new Map(this.definitions);
|
|
96
|
+
const batchKeys = new Set();
|
|
97
|
+
for (const { definition } of registrations) {
|
|
98
|
+
const key = (0, types_1.contractKey)(definition.contractId, definition.version);
|
|
99
|
+
if (batchKeys.has(key)) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
kind: "contract",
|
|
105
|
+
name: key,
|
|
106
|
+
message: `Provider ${providerPackageId} registered contract ${key} more than once`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
batchKeys.add(key);
|
|
112
|
+
const existingDef = this.definitions.get(key);
|
|
113
|
+
if (existingDef) {
|
|
114
|
+
const immutability = (0, validate_1.validateImmutability)(existingDef, definition);
|
|
115
|
+
if (!immutability.valid) {
|
|
116
|
+
return immutability;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
candidateDefinitions.set(key, definition);
|
|
120
|
+
}
|
|
121
|
+
for (const { definition, alsoImplements = [] } of registrations) {
|
|
122
|
+
if (alsoImplements.length === 0) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const compatibility = (0, validate_1.validateAlsoImplements)(definition, alsoImplements, (contractId, version) => candidateDefinitions.get((0, types_1.contractKey)(contractId, version)));
|
|
126
|
+
if (!compatibility.valid) {
|
|
127
|
+
return compatibility;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { valid: true, errors: [] };
|
|
131
|
+
}
|
|
132
|
+
commitRegistrations(providerPackageId, registrations) {
|
|
133
|
+
const explicitKeys = new Set(registrations.map(({ definition }) => (0, types_1.contractKey)(definition.contractId, definition.version)));
|
|
134
|
+
for (const { definition } of registrations) {
|
|
135
|
+
this.commitProvider(providerPackageId, definition, definition);
|
|
136
|
+
}
|
|
137
|
+
for (const { definition, alsoImplements = [] } of registrations) {
|
|
138
|
+
for (const declaration of alsoImplements) {
|
|
139
|
+
const declaredVersion = declaration.version;
|
|
140
|
+
const versions = typeof declaredVersion === "number"
|
|
141
|
+
? [declaredVersion]
|
|
142
|
+
: Array.from({ length: declaredVersion.to - declaredVersion.from + 1 }, (_, index) => declaredVersion.from + index);
|
|
143
|
+
for (const version of versions) {
|
|
144
|
+
const key = (0, types_1.contractKey)(declaration.contractId, version);
|
|
145
|
+
if (explicitKeys.has(key)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const target = this.definitions.get(key);
|
|
149
|
+
if (target) {
|
|
150
|
+
this.commitProvider(providerPackageId, target, definition);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
commitProvider(providerPackageId, definition, implementation) {
|
|
34
157
|
const key = (0, types_1.contractKey)(definition.contractId, definition.version);
|
|
35
|
-
// Immutability check against any previously stored definition
|
|
36
|
-
const existingDef = this.definitions.get(key);
|
|
37
|
-
if (existingDef) {
|
|
38
|
-
const immResult = (0, validate_1.validateImmutability)(existingDef, definition);
|
|
39
|
-
if (!immResult.valid)
|
|
40
|
-
return immResult;
|
|
41
|
-
}
|
|
42
|
-
// alsoImplements check
|
|
43
|
-
if (alsoImplementsDecls.length > 0) {
|
|
44
|
-
const aiResult = (0, validate_1.validateAlsoImplements)(definition, alsoImplementsDecls, (cid, ver) => this.definitions.get((0, types_1.contractKey)(cid, ver)));
|
|
45
|
-
if (!aiResult.valid)
|
|
46
|
-
return aiResult;
|
|
47
|
-
}
|
|
48
|
-
// Store the definition (first-come for frozen, latest for dev)
|
|
49
158
|
this.definitions.set(key, definition);
|
|
50
|
-
const provider = {
|
|
51
|
-
|
|
159
|
+
const provider = {
|
|
160
|
+
providerPackageId,
|
|
161
|
+
definition,
|
|
162
|
+
implementationContractId: implementation.contractId,
|
|
163
|
+
implementationVersion: implementation.version,
|
|
164
|
+
};
|
|
52
165
|
const providers = this.allProviders.get(key) ?? [];
|
|
53
|
-
const existingIdx = providers.findIndex((
|
|
166
|
+
const existingIdx = providers.findIndex((item) => item.providerPackageId === providerPackageId);
|
|
54
167
|
if (existingIdx >= 0) {
|
|
55
168
|
providers[existingIdx] = provider;
|
|
56
169
|
}
|
|
@@ -58,17 +171,15 @@ class ContractRegistry {
|
|
|
58
171
|
providers.push(provider);
|
|
59
172
|
}
|
|
60
173
|
this.allProviders.set(key, providers);
|
|
61
|
-
|
|
62
|
-
if (!
|
|
174
|
+
const active = this.activeProviders.get(key);
|
|
175
|
+
if (!active || active.providerPackageId === providerPackageId) {
|
|
63
176
|
this.activeProviders.set(key, provider);
|
|
64
177
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
pkgKeys.push(key);
|
|
178
|
+
const packageKeys = this.packageContracts.get(providerPackageId) ?? [];
|
|
179
|
+
if (!packageKeys.includes(key)) {
|
|
180
|
+
packageKeys.push(key);
|
|
69
181
|
}
|
|
70
|
-
this.packageContracts.set(providerPackageId,
|
|
71
|
-
return { valid: true, errors: [] };
|
|
182
|
+
this.packageContracts.set(providerPackageId, packageKeys);
|
|
72
183
|
}
|
|
73
184
|
/**
|
|
74
185
|
* Switch the active provider for a contract to a different registered package.
|
|
@@ -106,6 +217,25 @@ class ContractRegistry {
|
|
|
106
217
|
const key = (0, types_1.contractKey)(contractId, version);
|
|
107
218
|
return (this.allProviders.get(key) ?? []).map((p) => p.providerPackageId);
|
|
108
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Resolve the concrete contract implementation selected for one provider.
|
|
222
|
+
*
|
|
223
|
+
* For a direct registration this matches the requested contract. For an
|
|
224
|
+
* `alsoImplements` alias it identifies the newer compatible implementation.
|
|
225
|
+
*
|
|
226
|
+
* @param contractId - Consumer-requested contract identity.
|
|
227
|
+
* @param version - Consumer-requested contract version.
|
|
228
|
+
* @param providerPackageId - Registry-selected provider package.
|
|
229
|
+
*/
|
|
230
|
+
getProviderImplementation(contractId, version, providerPackageId) {
|
|
231
|
+
const provider = (this.allProviders.get((0, types_1.contractKey)(contractId, version)) ?? []).find((candidate) => candidate.providerPackageId === providerPackageId);
|
|
232
|
+
return provider
|
|
233
|
+
? {
|
|
234
|
+
contractId: provider.implementationContractId,
|
|
235
|
+
version: provider.implementationVersion,
|
|
236
|
+
}
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
109
239
|
/**
|
|
110
240
|
* Pick a provider for a consumed-contract declaration.
|
|
111
241
|
*
|
|
@@ -25,7 +25,9 @@ function registerSystemContract(registration) {
|
|
|
25
25
|
const { contractId, version, resolve } = registration;
|
|
26
26
|
queueMicrotask(() => {
|
|
27
27
|
const { registerContractResolver } = require("./contract-resolvers");
|
|
28
|
-
registerContractResolver(contractId, version, resolve
|
|
28
|
+
registerContractResolver(contractId, version, resolve, {
|
|
29
|
+
providerPackageId: registration.providerPackageId,
|
|
30
|
+
});
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
const key = registration.contractId && registration.version != null
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserContext } from "../context/user-context";
|
|
1
2
|
import { type IGroupMember } from "./group-members";
|
|
2
3
|
import { type IGroup } from "./groups";
|
|
3
4
|
/**
|
|
@@ -16,6 +17,18 @@ export interface IGroupShare {
|
|
|
16
17
|
* @throws Error if group not found or user lacks permission
|
|
17
18
|
*/
|
|
18
19
|
export declare function generateGroupShare(groupId: string): Promise<IGroupShare>;
|
|
20
|
+
/**
|
|
21
|
+
* Write a group record and missing membership rows into the group's data context.
|
|
22
|
+
*
|
|
23
|
+
* Uses the lowest data source so bootstrap can seed already-signed rows without
|
|
24
|
+
* re-running table permission checks. Existing memberships for the same user
|
|
25
|
+
* are left in place so retries can repair a partial import.
|
|
26
|
+
*
|
|
27
|
+
* @param userContext - Joiner or importer whose group context should be seeded
|
|
28
|
+
* @param groupShare - Canonical group plus Admin+ memberships
|
|
29
|
+
* @param extraMembers - Additional signed rows such as a newly approved Reader
|
|
30
|
+
*/
|
|
31
|
+
export declare function seedGroupContextFromShare(userContext: UserContext, groupShare: Pick<IGroupShare, "group" | "groupMembers">, extraMembers?: IGroupMember[]): Promise<void>;
|
|
19
32
|
/**
|
|
20
33
|
* Validates that an object has the correct GroupShare structure
|
|
21
34
|
* @param obj Object to validate
|
package/dist/data/group-share.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateGroupShare = generateGroupShare;
|
|
4
|
+
exports.seedGroupContextFromShare = seedGroupContextFromShare;
|
|
4
5
|
exports.validateGroupShare = validateGroupShare;
|
|
5
6
|
exports.estimateGroupShareSize = estimateGroupShareSize;
|
|
6
7
|
exports.checkQRCodeCompatibility = checkQRCodeCompatibility;
|
|
@@ -57,7 +58,7 @@ async function importGroupShare(groupShareJson) {
|
|
|
57
58
|
throw new Error("Invalid GroupShare structure");
|
|
58
59
|
}
|
|
59
60
|
const userContext = await (0, user_context_singleton_1.getUserContext)();
|
|
60
|
-
const { group
|
|
61
|
+
const { group } = groupShare;
|
|
61
62
|
// Check if group already exists
|
|
62
63
|
const existingGroup = await (0, groups_1.Groups)(userContext.userDataContext).get(group.groupId);
|
|
63
64
|
if (existingGroup) {
|
|
@@ -65,19 +66,42 @@ async function importGroupShare(groupShareJson) {
|
|
|
65
66
|
}
|
|
66
67
|
// Save group to user's groups table
|
|
67
68
|
await (0, groups_1.Groups)(userContext.userDataContext).save(group, { restoreIfDeleted: true });
|
|
68
|
-
|
|
69
|
+
await seedGroupContextFromShare(userContext, groupShare);
|
|
70
|
+
return group.groupId;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Write a group record and missing membership rows into the group's data context.
|
|
74
|
+
*
|
|
75
|
+
* Uses the lowest data source so bootstrap can seed already-signed rows without
|
|
76
|
+
* re-running table permission checks. Existing memberships for the same user
|
|
77
|
+
* are left in place so retries can repair a partial import.
|
|
78
|
+
*
|
|
79
|
+
* @param userContext - Joiner or importer whose group context should be seeded
|
|
80
|
+
* @param groupShare - Canonical group plus Admin+ memberships
|
|
81
|
+
* @param extraMembers - Additional signed rows such as a newly approved Reader
|
|
82
|
+
*/
|
|
83
|
+
async function seedGroupContextFromShare(userContext, groupShare, extraMembers = []) {
|
|
84
|
+
const { group, groupMembers } = groupShare;
|
|
69
85
|
const groupDataContext = userContext.getDataContext(group.groupId);
|
|
70
|
-
// Save the group data to the group's own context
|
|
71
86
|
const groupsTable = (0, groups_1.Groups)(groupDataContext);
|
|
72
|
-
|
|
73
|
-
await groupsTableDataSource.save(group);
|
|
74
|
-
// Save all admin members as seed data for device recognition
|
|
87
|
+
await getLowestDataSource(groupsTable).save(group);
|
|
75
88
|
const groupMembersTable = (0, group_members_1.GroupMembers)(groupDataContext);
|
|
76
89
|
const groupMembersDataSource = getLowestDataSource(groupMembersTable);
|
|
77
|
-
|
|
90
|
+
const seenUserIds = new Set();
|
|
91
|
+
for (const member of [...groupMembers, ...extraMembers]) {
|
|
92
|
+
if (!member?.userId || member.groupId !== group.groupId || seenUserIds.has(member.userId)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seenUserIds.add(member.userId);
|
|
96
|
+
const existing = await groupMembersTable.findOne({
|
|
97
|
+
groupId: group.groupId,
|
|
98
|
+
userId: member.userId,
|
|
99
|
+
});
|
|
100
|
+
if (existing) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
78
103
|
await groupMembersDataSource.save(member);
|
|
79
104
|
}
|
|
80
|
-
return group.groupId;
|
|
81
105
|
}
|
|
82
106
|
/**
|
|
83
107
|
* Validates that an object has the correct GroupShare structure
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const SQLiteDB = require("better-sqlite3");
|
|
4
|
+
const user_context_1 = require("../context/user-context");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const group_member_roles_1 = require("./group-member-roles");
|
|
7
|
+
const group_members_1 = require("./group-members");
|
|
8
|
+
const group_share_1 = require("./group-share");
|
|
9
|
+
const groups_1 = require("./groups");
|
|
10
|
+
const sql_data_source_1 = require("./orm/sql.data-source");
|
|
11
|
+
class DBHarness {
|
|
12
|
+
_db = null;
|
|
13
|
+
get db() {
|
|
14
|
+
if (!this._db) {
|
|
15
|
+
this._db = new SQLiteDB(":memory:");
|
|
16
|
+
this._db.pragma("journal_mode = WAL");
|
|
17
|
+
}
|
|
18
|
+
return this._db;
|
|
19
|
+
}
|
|
20
|
+
async get(sql, params = []) {
|
|
21
|
+
return this.db.prepare(sql).get(params);
|
|
22
|
+
}
|
|
23
|
+
async all(sql, params = []) {
|
|
24
|
+
return this.db.prepare(sql).all(params);
|
|
25
|
+
}
|
|
26
|
+
async exec(sql, params = []) {
|
|
27
|
+
const result = this.db.prepare(sql).run(params);
|
|
28
|
+
return { changes: result.changes };
|
|
29
|
+
}
|
|
30
|
+
async close() {
|
|
31
|
+
this._db?.close();
|
|
32
|
+
this._db = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
describe("seedGroupContextFromShare", () => {
|
|
36
|
+
const dbs = new Map();
|
|
37
|
+
let userContext;
|
|
38
|
+
const userId = (0, utils_1.newid)();
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
groups_1.GroupsTable.isPassthrough = true;
|
|
41
|
+
group_members_1.GroupMembersTable.isPassthrough = true;
|
|
42
|
+
const dataSourceFactory = (metaData, schema, groupId) => {
|
|
43
|
+
const key = groupId || userId;
|
|
44
|
+
let db = dbs.get(key);
|
|
45
|
+
if (!db) {
|
|
46
|
+
db = new DBHarness();
|
|
47
|
+
dbs.set(key, db);
|
|
48
|
+
}
|
|
49
|
+
return new sql_data_source_1.SQLDataSource(db, metaData, schema);
|
|
50
|
+
};
|
|
51
|
+
userContext = new user_context_1.UserContext(userId, dataSourceFactory, true);
|
|
52
|
+
await userContext.loadingPromise;
|
|
53
|
+
userContext.deviceId((0, utils_1.newid)());
|
|
54
|
+
});
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
groups_1.GroupsTable.isPassthrough = false;
|
|
57
|
+
group_members_1.GroupMembersTable.isPassthrough = false;
|
|
58
|
+
userContext.dispose();
|
|
59
|
+
await Promise.all([...dbs.values()].map((db) => db.close()));
|
|
60
|
+
});
|
|
61
|
+
function makeGroup() {
|
|
62
|
+
return {
|
|
63
|
+
groupId: (0, utils_1.newid)(),
|
|
64
|
+
name: "Home",
|
|
65
|
+
description: "",
|
|
66
|
+
founderUserId: (0, utils_1.newid)(),
|
|
67
|
+
signature: "group-sig",
|
|
68
|
+
publicKey: "pk",
|
|
69
|
+
publicBoxKey: "pbk",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function makeMember(groupId, memberUserId, role) {
|
|
73
|
+
return {
|
|
74
|
+
groupMemberId: (0, utils_1.newid)(),
|
|
75
|
+
groupId,
|
|
76
|
+
userId: memberUserId,
|
|
77
|
+
role,
|
|
78
|
+
signature: "member-sig",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
it("seeds the group record and missing memberships, including extra Reader rows", async () => {
|
|
82
|
+
const group = makeGroup();
|
|
83
|
+
const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
|
|
84
|
+
const joiner = makeMember(group.groupId, userId, group_member_roles_1.GroupMemberRole.Reader);
|
|
85
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] }, [joiner]);
|
|
86
|
+
const groupContext = userContext.getDataContext(group.groupId);
|
|
87
|
+
await expect((0, groups_1.Groups)(groupContext).get(group.groupId)).resolves.toMatchObject({
|
|
88
|
+
name: "Home",
|
|
89
|
+
});
|
|
90
|
+
await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: admin.userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Admin });
|
|
91
|
+
await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Reader });
|
|
92
|
+
});
|
|
93
|
+
it("leaves existing memberships in place on a retry", async () => {
|
|
94
|
+
const group = makeGroup();
|
|
95
|
+
const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
|
|
96
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] });
|
|
97
|
+
const replacement = {
|
|
98
|
+
...admin,
|
|
99
|
+
groupMemberId: (0, utils_1.newid)(),
|
|
100
|
+
signature: "other-sig",
|
|
101
|
+
};
|
|
102
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [replacement] });
|
|
103
|
+
const saved = await (0, group_members_1.GroupMembers)(userContext.getDataContext(group.groupId)).findOne({
|
|
104
|
+
userId: admin.userId,
|
|
105
|
+
groupId: group.groupId,
|
|
106
|
+
});
|
|
107
|
+
expect(saved?.groupMemberId).toBe(admin.groupMemberId);
|
|
108
|
+
expect(saved?.signature).toBe("member-sig");
|
|
109
|
+
});
|
|
110
|
+
});
|
package/dist/data/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./group-secret";
|
|
|
13
13
|
export * from "./group-share";
|
|
14
14
|
export * from "./groups";
|
|
15
15
|
export * from "./messages";
|
|
16
|
+
export * from "./package-table-identity";
|
|
16
17
|
export * from "./package-version-resolver";
|
|
17
18
|
export * from "./package-versions";
|
|
18
19
|
export * from "./packages";
|
package/dist/data/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __exportStar(require("./group-secret"), exports);
|
|
|
29
29
|
__exportStar(require("./group-share"), exports);
|
|
30
30
|
__exportStar(require("./groups"), exports);
|
|
31
31
|
__exportStar(require("./messages"), exports);
|
|
32
|
+
__exportStar(require("./package-table-identity"), exports);
|
|
32
33
|
__exportStar(require("./package-version-resolver"), exports);
|
|
33
34
|
__exportStar(require("./package-versions"), exports);
|
|
34
35
|
__exportStar(require("./packages"), exports);
|
package/dist/data/orm/types.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type IField } from "../../types/field-type";
|
|
3
|
+
/** Pure-data schema and storage metadata for a Peers table. */
|
|
3
4
|
export interface ITableMetaData {
|
|
4
5
|
tableId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Trusted owning package for package-scoped physical naming.
|
|
8
|
+
*
|
|
9
|
+
* When present, physical naming uses `name_packageId` while `tableId`
|
|
10
|
+
* remains the independent schema-definition identity.
|
|
11
|
+
*/
|
|
12
|
+
packageId?: string;
|
|
5
13
|
name: string;
|
|
6
14
|
description: string;
|
|
7
15
|
primaryKeyName: string;
|
|
@@ -17,5 +25,16 @@ export interface ITableMetaData {
|
|
|
17
25
|
localOnly?: boolean;
|
|
18
26
|
versionNumber?: number;
|
|
19
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert field metadata to the runtime Zod schema used by table validation.
|
|
30
|
+
*
|
|
31
|
+
* @param fields Serializable field definitions.
|
|
32
|
+
* @param codeSchema Optional author schema whose refinements are intersected.
|
|
33
|
+
*/
|
|
20
34
|
export declare function fieldsToSchema(fields: IField[], codeSchema?: z.AnyZodObject): z.AnyZodObject;
|
|
35
|
+
/**
|
|
36
|
+
* Convert a Zod object schema to serializable field metadata.
|
|
37
|
+
*
|
|
38
|
+
* @param schema Runtime record schema.
|
|
39
|
+
*/
|
|
21
40
|
export declare function schemaToFields(schema: z.AnyZodObject): IField[];
|
package/dist/data/orm/types.js
CHANGED
|
@@ -5,6 +5,12 @@ exports.schemaToFields = schemaToFields;
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const field_type_1 = require("../../types/field-type");
|
|
7
7
|
const utils_1 = require("../../utils");
|
|
8
|
+
/**
|
|
9
|
+
* Convert field metadata to the runtime Zod schema used by table validation.
|
|
10
|
+
*
|
|
11
|
+
* @param fields Serializable field definitions.
|
|
12
|
+
* @param codeSchema Optional author schema whose refinements are intersected.
|
|
13
|
+
*/
|
|
8
14
|
function fieldsToSchema(fields, codeSchema) {
|
|
9
15
|
let argsSchema = zod_1.z.object({});
|
|
10
16
|
for (const argSchema of fields) {
|
|
@@ -52,6 +58,11 @@ function fieldsToSchema(fields, codeSchema) {
|
|
|
52
58
|
}
|
|
53
59
|
return argsSchema;
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Convert a Zod object schema to serializable field metadata.
|
|
63
|
+
*
|
|
64
|
+
* @param schema Runtime record schema.
|
|
65
|
+
*/
|
|
55
66
|
function schemaToFields(schema) {
|
|
56
67
|
const fields = [];
|
|
57
68
|
for (const name of Object.keys(schema.shape)) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a guest-supplied isolated-table or field identifier before it can
|
|
3
|
+
* reach ORM-generated SQL.
|
|
4
|
+
*
|
|
5
|
+
* This intentionally accepts only simple SQLite identifiers. The package
|
|
6
|
+
* manifest validator and host gateway can share this helper so malformed
|
|
7
|
+
* names are rejected even when a declaration bypasses artifact loading.
|
|
8
|
+
*
|
|
9
|
+
* @param identifier Guest-supplied logical table or field name.
|
|
10
|
+
* @param label Description used in validation errors.
|
|
11
|
+
* @returns The unchanged identifier.
|
|
12
|
+
* @throws When the identifier is empty or contains SQL syntax.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateIsolatedSqlIdentifier(identifier: string, label?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Validate a package-local table name.
|
|
17
|
+
*
|
|
18
|
+
* @param logicalName Guest-visible table name.
|
|
19
|
+
* @returns The unchanged logical name.
|
|
20
|
+
* @throws When the name is not a SQL-safe isolated identifier.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validatePackageTableName(logicalName: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Derive the physical SQLite name for a package-owned table.
|
|
25
|
+
*
|
|
26
|
+
* Package ID is appended so authors only need unique logical names inside
|
|
27
|
+
* their own package.
|
|
28
|
+
*
|
|
29
|
+
* @param packageId Trusted owning package ID.
|
|
30
|
+
* @param logicalName Package-local table name.
|
|
31
|
+
* @returns The package-scoped physical table name.
|
|
32
|
+
* @throws When package or table identity is invalid.
|
|
33
|
+
*/
|
|
34
|
+
export declare function packageScopedTableName(packageId: string, logicalName: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Derive the stable host-only ID used to key `TableDefinitions`.
|
|
37
|
+
*
|
|
38
|
+
* The ID is intentionally separate from the physical SQLite name so schema
|
|
39
|
+
* tracking keeps a valid peer ID without producing a triple-suffixed table.
|
|
40
|
+
*
|
|
41
|
+
* @param packageId Trusted owning package ID.
|
|
42
|
+
* @param logicalName Package-local table name.
|
|
43
|
+
* @returns A deterministic peer ID for the package-owned table definition.
|
|
44
|
+
* @throws When package or table identity is invalid.
|
|
45
|
+
*/
|
|
46
|
+
export declare function packageScopedTableId(packageId: string, logicalName: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateIsolatedSqlIdentifier = validateIsolatedSqlIdentifier;
|
|
4
|
+
exports.validatePackageTableName = validatePackageTableName;
|
|
5
|
+
exports.packageScopedTableName = packageScopedTableName;
|
|
6
|
+
exports.packageScopedTableId = packageScopedTableId;
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const ISOLATED_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
/**
|
|
10
|
+
* Validate a guest-supplied isolated-table or field identifier before it can
|
|
11
|
+
* reach ORM-generated SQL.
|
|
12
|
+
*
|
|
13
|
+
* This intentionally accepts only simple SQLite identifiers. The package
|
|
14
|
+
* manifest validator and host gateway can share this helper so malformed
|
|
15
|
+
* names are rejected even when a declaration bypasses artifact loading.
|
|
16
|
+
*
|
|
17
|
+
* @param identifier Guest-supplied logical table or field name.
|
|
18
|
+
* @param label Description used in validation errors.
|
|
19
|
+
* @returns The unchanged identifier.
|
|
20
|
+
* @throws When the identifier is empty or contains SQL syntax.
|
|
21
|
+
*/
|
|
22
|
+
function validateIsolatedSqlIdentifier(identifier, label = "Isolated SQL identifier") {
|
|
23
|
+
if (typeof identifier !== "string" || identifier.trim() === "") {
|
|
24
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
25
|
+
}
|
|
26
|
+
if (!ISOLATED_SQL_IDENTIFIER.test(identifier)) {
|
|
27
|
+
throw new Error(`${label} must start with a letter or underscore and contain only letters, numbers, and underscores`);
|
|
28
|
+
}
|
|
29
|
+
return identifier;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate a package-local table name.
|
|
33
|
+
*
|
|
34
|
+
* @param logicalName Guest-visible table name.
|
|
35
|
+
* @returns The unchanged logical name.
|
|
36
|
+
* @throws When the name is not a SQL-safe isolated identifier.
|
|
37
|
+
*/
|
|
38
|
+
function validatePackageTableName(logicalName) {
|
|
39
|
+
return validateIsolatedSqlIdentifier(logicalName, "Package table name");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Derive the physical SQLite name for a package-owned table.
|
|
43
|
+
*
|
|
44
|
+
* Package ID is appended so authors only need unique logical names inside
|
|
45
|
+
* their own package.
|
|
46
|
+
*
|
|
47
|
+
* @param packageId Trusted owning package ID.
|
|
48
|
+
* @param logicalName Package-local table name.
|
|
49
|
+
* @returns The package-scoped physical table name.
|
|
50
|
+
* @throws When package or table identity is invalid.
|
|
51
|
+
*/
|
|
52
|
+
function packageScopedTableName(packageId, logicalName) {
|
|
53
|
+
if (!(0, utils_1.isid)(packageId)) {
|
|
54
|
+
throw new Error(`Invalid package ID for table: ${packageId}`);
|
|
55
|
+
}
|
|
56
|
+
return `${validatePackageTableName(logicalName)}_${packageId}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Derive the stable host-only ID used to key `TableDefinitions`.
|
|
60
|
+
*
|
|
61
|
+
* The ID is intentionally separate from the physical SQLite name so schema
|
|
62
|
+
* tracking keeps a valid peer ID without producing a triple-suffixed table.
|
|
63
|
+
*
|
|
64
|
+
* @param packageId Trusted owning package ID.
|
|
65
|
+
* @param logicalName Package-local table name.
|
|
66
|
+
* @returns A deterministic peer ID for the package-owned table definition.
|
|
67
|
+
* @throws When package or table identity is invalid.
|
|
68
|
+
*/
|
|
69
|
+
function packageScopedTableId(packageId, logicalName) {
|
|
70
|
+
return (0, utils_1.deterministicTableId)(packageScopedTableName(packageId, logicalName));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("../utils");
|
|
4
|
+
const package_table_identity_1 = require("./package-table-identity");
|
|
5
|
+
describe("package-scoped table identity", () => {
|
|
6
|
+
test("appends trusted package identity to the logical table name", () => {
|
|
7
|
+
const packageId = (0, utils_1.newid)();
|
|
8
|
+
const tableId = (0, package_table_identity_1.packageScopedTableId)(packageId, "Settings");
|
|
9
|
+
const metaData = {
|
|
10
|
+
packageId,
|
|
11
|
+
tableId,
|
|
12
|
+
name: "Settings",
|
|
13
|
+
description: "Package settings",
|
|
14
|
+
primaryKeyName: "settingId",
|
|
15
|
+
fields: [],
|
|
16
|
+
};
|
|
17
|
+
expect((0, package_table_identity_1.packageScopedTableName)(packageId, "Settings")).toBe(`Settings_${packageId}`);
|
|
18
|
+
expect((0, utils_1.getFullTableName)(metaData)).toBe(`Settings_${packageId}`);
|
|
19
|
+
expect((0, utils_1.getFullTableName)(metaData)).not.toContain(tableId);
|
|
20
|
+
expect((0, utils_1.isid)(tableId)).toBe(true);
|
|
21
|
+
expect((0, utils_1.isDeterministicTableId)(tableId)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
test("keeps existing table naming unchanged", () => {
|
|
24
|
+
const tableId = (0, utils_1.newid)();
|
|
25
|
+
expect((0, utils_1.getFullTableName)({
|
|
26
|
+
tableId,
|
|
27
|
+
name: "Settings",
|
|
28
|
+
description: "Existing table",
|
|
29
|
+
primaryKeyName: "settingId",
|
|
30
|
+
fields: [],
|
|
31
|
+
})).toBe(`Settings_${tableId}`);
|
|
32
|
+
});
|
|
33
|
+
test("uses package and logical name for stable internal IDs", () => {
|
|
34
|
+
const packageId = (0, utils_1.newid)();
|
|
35
|
+
expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).toBe((0, package_table_identity_1.packageScopedTableId)(packageId, "One"));
|
|
36
|
+
expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).not.toBe((0, package_table_identity_1.packageScopedTableId)(packageId, "Two"));
|
|
37
|
+
expect((0, package_table_identity_1.packageScopedTableId)(packageId, "One")).not.toBe((0, package_table_identity_1.packageScopedTableId)((0, utils_1.newid)(), "One"));
|
|
38
|
+
});
|
|
39
|
+
test("rejects invalid package IDs and empty logical names", () => {
|
|
40
|
+
expect(() => (0, package_table_identity_1.packageScopedTableName)("not-an-id", "Settings")).toThrow(/package ID/);
|
|
41
|
+
expect(() => (0, package_table_identity_1.packageScopedTableName)((0, utils_1.newid)(), " ")).toThrow(/non-empty/);
|
|
42
|
+
expect(() => (0, package_table_identity_1.packageScopedTableName)((0, utils_1.newid)(), 'State"; DROP TABLE Users; --')).toThrow(/only letters, numbers, and underscores/);
|
|
43
|
+
expect((0, package_table_identity_1.validateIsolatedSqlIdentifier)("state_value", "field")).toBe("state_value");
|
|
44
|
+
expect(() => (0, package_table_identity_1.validateIsolatedSqlIdentifier)("state.value", "field")).toThrow(/field/);
|
|
45
|
+
});
|
|
46
|
+
});
|