@phis/contracts 0.3.1 → 0.5.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.
- package/NOTICE +1 -1
- package/README.md +159 -151
- package/dist/access.d.ts +13 -0
- package/dist/access.js +13 -0
- package/dist/addon-index.d.ts +4 -0
- package/dist/addon-index.js +4 -0
- package/dist/addon.d.ts +53 -2
- package/dist/capabilities.d.ts +54 -87
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +14 -2
- package/dist/cms.d.ts +11 -0
- package/dist/cms.js +20 -0
- package/dist/conditions.d.ts +96 -0
- package/dist/conditions.js +163 -0
- package/dist/controls.d.ts +83 -0
- package/dist/controls.js +133 -0
- package/dist/core.d.ts +38 -0
- package/dist/core.js +39 -0
- package/dist/forms.d.ts +496 -0
- package/dist/forms.js +508 -0
- package/dist/http.d.ts +10 -1
- package/dist/http.js +10 -1
- package/dist/layout.d.ts +22 -0
- package/dist/layout.js +30 -0
- package/dist/media-storage.d.ts +256 -14
- package/dist/media-storage.js +30 -2
- package/dist/media.d.ts +47 -3
- package/dist/media.js +59 -0
- package/dist/record.d.ts +8 -0
- package/dist/record.js +24 -0
- package/dist/references.d.ts +130 -0
- package/dist/references.js +208 -0
- package/dist/resource-links.d.ts +94 -0
- package/dist/resource-links.js +27 -0
- package/dist/signals.d.ts +1 -0
- package/dist/signals.js +3 -0
- package/dist/site-groups.d.ts +15 -0
- package/dist/support.d.ts +390 -0
- package/dist/support.js +148 -0
- package/dist/threads.d.ts +363 -0
- package/dist/threads.js +146 -0
- package/dist/translation.d.ts +125 -0
- package/dist/translation.js +15 -0
- package/dist/user-state.d.ts +132 -0
- package/dist/user-state.js +92 -0
- package/package.json +47 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one side of the house means when it points at a Page: `@phis/server/references:v1`.
|
|
3
|
+
*
|
|
4
|
+
* The codec stood in two copies, in phis-ui and in phi-server, for the same reason the CMS instance id
|
|
5
|
+
* did before it moved here -- and with the same risk, which is that two sides agreeing byte for byte is
|
|
6
|
+
* a thing nobody notices stopping. It is one file now.
|
|
7
|
+
*
|
|
8
|
+
* **A reference is short because a Page already has a name.** It used to be base64-encoded JSON carrying
|
|
9
|
+
* the whole identity, which for a Module Page ran to 98 characters in every Markdown body that linked to
|
|
10
|
+
* one. The envelope was never protection -- it is encoding, not a signature, and the server revalidates
|
|
11
|
+
* Site and Area regardless -- so what it actually bought was a payload the server could unpack. It no
|
|
12
|
+
* longer needs to: a Module Page is named by the CMS Page id derived from `(ownerModuleId, presetKey)`,
|
|
13
|
+
* which is recomputable rather than allocated and therefore stands before any row does.
|
|
14
|
+
*
|
|
15
|
+
* The two variants are shaped differently on purpose. A Site Page carries its Page Scope id as a plain
|
|
16
|
+
* number because the server writes that number into a foreign key with `ON DELETE RESTRICT`: the
|
|
17
|
+
* database itself refuses to delete a Page something still points at, and nothing derived could take
|
|
18
|
+
* that column's place. A Module Page has no row to point a foreign key at -- the Site may never have
|
|
19
|
+
* touched it -- so it carries the derived id, which the server passes on without ever unpacking.
|
|
20
|
+
*/
|
|
21
|
+
import { type PhiCmsAreaKey, type PhiCmsInstanceId } from "./cms.js";
|
|
22
|
+
export declare const PHIS_INTERNAL_PAGE_SCHEME: "phis:page/";
|
|
23
|
+
export declare const PHIS_INTERNAL_ASSET_SCHEME: "phis:asset/";
|
|
24
|
+
/**
|
|
25
|
+
* A Module Page's identity, hashed to one token both sides can recompute.
|
|
26
|
+
*
|
|
27
|
+
* Recomputable rather than allocated, so no row has to exist before the Page can be addressed, and it
|
|
28
|
+
* never mentions the path: reassigning where a Page answers leaves everything pointing at it alone.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createPhiPresetCmsPageId(identity: {
|
|
31
|
+
ownerModuleId: string;
|
|
32
|
+
presetKey: string;
|
|
33
|
+
}): PhiCmsInstanceId;
|
|
34
|
+
/** What a reference says, once read. A Module Page answers with its id, never with what was hashed. */
|
|
35
|
+
export type PhiPageTarget = {
|
|
36
|
+
kind: "site";
|
|
37
|
+
pageScopeId: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "module";
|
|
40
|
+
pageId: PhiCmsInstanceId;
|
|
41
|
+
};
|
|
42
|
+
/** What a reference is made from. The Module variant differs: the identity goes in, the id comes out. */
|
|
43
|
+
export type PhiPageTargetInput = {
|
|
44
|
+
kind: "site";
|
|
45
|
+
pageScopeId: number;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "module";
|
|
48
|
+
ownerModuleId: string;
|
|
49
|
+
presetKey: string;
|
|
50
|
+
};
|
|
51
|
+
export type PhiPageReference = string & {
|
|
52
|
+
readonly __phiPageReference: unique symbol;
|
|
53
|
+
};
|
|
54
|
+
export type PhiInternalReference = {
|
|
55
|
+
kind: "page";
|
|
56
|
+
reference: PhiPageReference;
|
|
57
|
+
target: PhiPageTarget;
|
|
58
|
+
fragment: string | null;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "asset";
|
|
61
|
+
assetId: number;
|
|
62
|
+
};
|
|
63
|
+
export declare function createPhiPageReference(target: PhiPageTargetInput): PhiPageReference;
|
|
64
|
+
export declare function readPhiPageReference(value: unknown): {
|
|
65
|
+
reference: PhiPageReference;
|
|
66
|
+
target: PhiPageTarget;
|
|
67
|
+
} | null;
|
|
68
|
+
export declare function createPhiPageUri(reference: PhiPageReference, fragment?: string | null): string;
|
|
69
|
+
export declare function createPhiAssetUri(assetId: number): string;
|
|
70
|
+
export declare function readPhiInternalReference(value: unknown): PhiInternalReference | null;
|
|
71
|
+
/**
|
|
72
|
+
* Where a Control that offers one link leads.
|
|
73
|
+
*
|
|
74
|
+
* Two kinds and no third. A Page is named by its reference, which is identity and outlives the path
|
|
75
|
+
* moving under it; everything else is a literal URL somebody typed. What this refuses is the middle --
|
|
76
|
+
* the root-relative string that reads as external and means internal -- because that is the shape that
|
|
77
|
+
* rots in silence: the Page moves, the string does not, and nothing in the system ever knew the two were
|
|
78
|
+
* related.
|
|
79
|
+
*
|
|
80
|
+
* `newTab` sits inside the target rather than beside it. A Widget that offers two links -- a Card with a
|
|
81
|
+
* heading and an action -- otherwise grows one flag per link and names them in parallel (`newTab`,
|
|
82
|
+
* `actionNewTab`, and a third the day a third link lands). That is one decision written twice, and it
|
|
83
|
+
* drifts the first time only one of the two is read.
|
|
84
|
+
*/
|
|
85
|
+
export type PhiLinkTarget = {
|
|
86
|
+
kind: "page";
|
|
87
|
+
reference: PhiPageReference;
|
|
88
|
+
/**
|
|
89
|
+
* Which Area to resolve the reference in, where it is not the one asking.
|
|
90
|
+
*
|
|
91
|
+
* A reference names a Page and not where to look for it: the Area is resolution context, supplied
|
|
92
|
+
* by whoever asks. Every asker supplied its own, so a link out of one Area into another resolved
|
|
93
|
+
* to nothing -- the Navigation source tree let an author pick a Page from another Area, the drop
|
|
94
|
+
* kept it, and the table then showed it as unresolvable.
|
|
95
|
+
*
|
|
96
|
+
* Absent means the asking Area, which is nearly every link, so nothing is stored for the ordinary
|
|
97
|
+
* case. Present, it does not make the reference mean something else; it says where the question
|
|
98
|
+
* goes. Whether the reader may follow it is still the target Area's own answer.
|
|
99
|
+
*/
|
|
100
|
+
area?: PhiCmsAreaKey;
|
|
101
|
+
fragment?: string | null;
|
|
102
|
+
newTab?: boolean;
|
|
103
|
+
} | {
|
|
104
|
+
kind: "external";
|
|
105
|
+
href: string;
|
|
106
|
+
newTab?: boolean;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The field names a stored link target may stand under, and nothing else identifies one in persisted
|
|
110
|
+
* config.
|
|
111
|
+
*
|
|
112
|
+
* The Asset rule is the precedent and the reason: the server's reference collector reads persisted JSON,
|
|
113
|
+
* not the Widget catalogue, so it cannot ask a plugin which of its fields hold targets. A Widget that
|
|
114
|
+
* keeps one under another name authors a reference the delete guard cannot see, and the Page it points
|
|
115
|
+
* at can be removed while the Widget still draws a link to it.
|
|
116
|
+
*
|
|
117
|
+
* One plain name for the single-link case and a suffix for the rest, so a Card's second link is
|
|
118
|
+
* `actionLinkTarget` and is found by the same rule that finds the first.
|
|
119
|
+
*/
|
|
120
|
+
export declare const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
|
|
121
|
+
export declare function isPhiLinkTargetConfigKey(key: string): boolean;
|
|
122
|
+
export declare function isPhiStorableExternalHref(value: string): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* A stored link target, read back under the contract rather than trusted.
|
|
125
|
+
*
|
|
126
|
+
* Anything that is not one of the two shapes answers `null`, and a Control that gets `null` draws no
|
|
127
|
+
* link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
|
|
128
|
+
* non-interactive text beats a link that goes somewhere nobody chose.
|
|
129
|
+
*/
|
|
130
|
+
export declare function readPhiLinkTarget(value: unknown): PhiLinkTarget | null;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one side of the house means when it points at a Page: `@phis/server/references:v1`.
|
|
3
|
+
*
|
|
4
|
+
* The codec stood in two copies, in phis-ui and in phi-server, for the same reason the CMS instance id
|
|
5
|
+
* did before it moved here -- and with the same risk, which is that two sides agreeing byte for byte is
|
|
6
|
+
* a thing nobody notices stopping. It is one file now.
|
|
7
|
+
*
|
|
8
|
+
* **A reference is short because a Page already has a name.** It used to be base64-encoded JSON carrying
|
|
9
|
+
* the whole identity, which for a Module Page ran to 98 characters in every Markdown body that linked to
|
|
10
|
+
* one. The envelope was never protection -- it is encoding, not a signature, and the server revalidates
|
|
11
|
+
* Site and Area regardless -- so what it actually bought was a payload the server could unpack. It no
|
|
12
|
+
* longer needs to: a Module Page is named by the CMS Page id derived from `(ownerModuleId, presetKey)`,
|
|
13
|
+
* which is recomputable rather than allocated and therefore stands before any row does.
|
|
14
|
+
*
|
|
15
|
+
* The two variants are shaped differently on purpose. A Site Page carries its Page Scope id as a plain
|
|
16
|
+
* number because the server writes that number into a foreign key with `ON DELETE RESTRICT`: the
|
|
17
|
+
* database itself refuses to delete a Page something still points at, and nothing derived could take
|
|
18
|
+
* that column's place. A Module Page has no row to point a foreign key at -- the Site may never have
|
|
19
|
+
* touched it -- so it carries the derived id, which the server passes on without ever unpacking.
|
|
20
|
+
*/
|
|
21
|
+
import { createPhiPresetCmsInstanceId, isPhiCmsAreaKey, readPhiCmsInstanceIdDescriptor, } from "./cms.js";
|
|
22
|
+
export const PHIS_INTERNAL_PAGE_SCHEME = "phis:page/";
|
|
23
|
+
export const PHIS_INTERNAL_ASSET_SCHEME = "phis:asset/";
|
|
24
|
+
/** The Page itself, not a node inside it -- the one place a Page id differs from a node id. */
|
|
25
|
+
const PHI_CMS_PAGE_NODE_KEY = "page";
|
|
26
|
+
const PHI_PAGE_REFERENCE_VERSION = "v1";
|
|
27
|
+
const PHI_PAGE_REFERENCE_SITE_TAG = "s";
|
|
28
|
+
const PHI_PAGE_REFERENCE_MODULE_TAG = "p";
|
|
29
|
+
const PHI_MODULE_ID_PATTERN = /^@[^/]+\/[^/]+(?:\/[^/]+)*$/;
|
|
30
|
+
const PHI_PAGE_SCOPE_ID_PATTERN = /^[1-9]\d*$/u;
|
|
31
|
+
/**
|
|
32
|
+
* A Module Page's identity, hashed to one token both sides can recompute.
|
|
33
|
+
*
|
|
34
|
+
* Recomputable rather than allocated, so no row has to exist before the Page can be addressed, and it
|
|
35
|
+
* never mentions the path: reassigning where a Page answers leaves everything pointing at it alone.
|
|
36
|
+
*/
|
|
37
|
+
export function createPhiPresetCmsPageId(identity) {
|
|
38
|
+
return createPhiPresetCmsInstanceId({
|
|
39
|
+
...identity,
|
|
40
|
+
domain: "page",
|
|
41
|
+
nodeKey: PHI_CMS_PAGE_NODE_KEY,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isPositiveInteger(value) {
|
|
45
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
46
|
+
}
|
|
47
|
+
function readPresetPageId(value) {
|
|
48
|
+
const descriptor = readPhiCmsInstanceIdDescriptor(value);
|
|
49
|
+
return descriptor?.origin === "preset" && descriptor.domain === "page"
|
|
50
|
+
? value
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
export function createPhiPageReference(target) {
|
|
54
|
+
if (target.kind === "site") {
|
|
55
|
+
if (!isPositiveInteger(target.pageScopeId)) {
|
|
56
|
+
throw new Error("Phi Page Scope ids must be positive integers.");
|
|
57
|
+
}
|
|
58
|
+
return `${PHI_PAGE_REFERENCE_VERSION}${PHI_PAGE_REFERENCE_SITE_TAG}${target.pageScopeId}`;
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
* Checked here rather than left to the hash. A malformed owner id hashes perfectly well and produces
|
|
62
|
+
* a reference that resolves to nothing, for a reason nobody can read back out of it -- the one real
|
|
63
|
+
* cost of a one-way id, paid once at the only place that can still see what went in.
|
|
64
|
+
*/
|
|
65
|
+
if (!PHI_MODULE_ID_PATTERN.test(target.ownerModuleId.trim())) {
|
|
66
|
+
throw new Error("Invalid Phi Module id.");
|
|
67
|
+
}
|
|
68
|
+
const pageId = createPhiPresetCmsPageId({
|
|
69
|
+
ownerModuleId: target.ownerModuleId,
|
|
70
|
+
presetKey: target.presetKey,
|
|
71
|
+
});
|
|
72
|
+
return `${PHI_PAGE_REFERENCE_VERSION}${PHI_PAGE_REFERENCE_MODULE_TAG}${pageId}`;
|
|
73
|
+
}
|
|
74
|
+
export function readPhiPageReference(value) {
|
|
75
|
+
if (typeof value !== "string" || !value.startsWith(PHI_PAGE_REFERENCE_VERSION)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const tag = value.slice(PHI_PAGE_REFERENCE_VERSION.length, PHI_PAGE_REFERENCE_VERSION.length + 1);
|
|
79
|
+
const body = value.slice(PHI_PAGE_REFERENCE_VERSION.length + 1);
|
|
80
|
+
if (tag === PHI_PAGE_REFERENCE_SITE_TAG) {
|
|
81
|
+
const pageScopeId = PHI_PAGE_SCOPE_ID_PATTERN.test(body) ? Number(body) : NaN;
|
|
82
|
+
return isPositiveInteger(pageScopeId)
|
|
83
|
+
? { reference: value, target: { kind: "site", pageScopeId } }
|
|
84
|
+
: null;
|
|
85
|
+
}
|
|
86
|
+
if (tag === PHI_PAGE_REFERENCE_MODULE_TAG) {
|
|
87
|
+
const pageId = readPresetPageId(body);
|
|
88
|
+
return pageId
|
|
89
|
+
? { reference: value, target: { kind: "module", pageId } }
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
export function createPhiPageUri(reference, fragment) {
|
|
95
|
+
if (!readPhiPageReference(reference)) {
|
|
96
|
+
throw new Error("Invalid Phi Page reference.");
|
|
97
|
+
}
|
|
98
|
+
const normalizedFragment = fragment?.replace(/^#/u, "").trim();
|
|
99
|
+
return `${PHIS_INTERNAL_PAGE_SCHEME}${reference}${normalizedFragment ? `#${encodeURIComponent(normalizedFragment)}` : ""}`;
|
|
100
|
+
}
|
|
101
|
+
export function createPhiAssetUri(assetId) {
|
|
102
|
+
if (!isPositiveInteger(assetId)) {
|
|
103
|
+
throw new Error("Phi Asset ids must be positive integers.");
|
|
104
|
+
}
|
|
105
|
+
return `${PHIS_INTERNAL_ASSET_SCHEME}${assetId}`;
|
|
106
|
+
}
|
|
107
|
+
export function readPhiInternalReference(value) {
|
|
108
|
+
if (typeof value !== "string") {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (value.startsWith(PHIS_INTERNAL_ASSET_SCHEME)) {
|
|
112
|
+
const id = value.slice(PHIS_INTERNAL_ASSET_SCHEME.length);
|
|
113
|
+
return /^[1-9]\d*$/u.test(id) && Number.isSafeInteger(Number(id))
|
|
114
|
+
? { kind: "asset", assetId: Number(id) }
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
if (!value.startsWith(PHIS_INTERNAL_PAGE_SCHEME)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const [encodedReference, ...fragmentParts] = value.slice(PHIS_INTERNAL_PAGE_SCHEME.length).split("#");
|
|
121
|
+
const parsed = readPhiPageReference(encodedReference);
|
|
122
|
+
if (!parsed) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const fragment = fragmentParts.length > 0 ? decodeURIComponent(fragmentParts.join("#")) : null;
|
|
127
|
+
return { kind: "page", ...parsed, fragment };
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The field names a stored link target may stand under, and nothing else identifies one in persisted
|
|
135
|
+
* config.
|
|
136
|
+
*
|
|
137
|
+
* The Asset rule is the precedent and the reason: the server's reference collector reads persisted JSON,
|
|
138
|
+
* not the Widget catalogue, so it cannot ask a plugin which of its fields hold targets. A Widget that
|
|
139
|
+
* keeps one under another name authors a reference the delete guard cannot see, and the Page it points
|
|
140
|
+
* at can be removed while the Widget still draws a link to it.
|
|
141
|
+
*
|
|
142
|
+
* One plain name for the single-link case and a suffix for the rest, so a Card's second link is
|
|
143
|
+
* `actionLinkTarget` and is found by the same rule that finds the first.
|
|
144
|
+
*/
|
|
145
|
+
export const PHI_LINK_TARGET_CONFIG_KEY = "linkTarget";
|
|
146
|
+
const PHI_LINK_TARGET_CONFIG_KEY_SUFFIX = "LinkTarget";
|
|
147
|
+
export function isPhiLinkTargetConfigKey(key) {
|
|
148
|
+
return key === PHI_LINK_TARGET_CONFIG_KEY
|
|
149
|
+
|| (key.endsWith(PHI_LINK_TARGET_CONFIG_KEY_SUFFIX) && key.length > PHI_LINK_TARGET_CONFIG_KEY_SUFFIX.length);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether a string may be kept as an external target.
|
|
153
|
+
*
|
|
154
|
+
* Absolute, or an anchor on the document the reader already has. Everything else is refused, and the
|
|
155
|
+
* refusal is the whole point rather than strictness for its own sake: a stored `/pricing` is a Page link
|
|
156
|
+
* that has thrown its identity away, and one of them is enough to give the Site a second internal format
|
|
157
|
+
* -- one the resolver cannot resolve, the index cannot see, and the delete guard cannot warn about.
|
|
158
|
+
*
|
|
159
|
+
* The reserved scheme is rejected here in its plain form only. Normalising the encodings it can hide in
|
|
160
|
+
* belongs to the server's sanitizer, which sees the whole document; this is the near guard, so a target
|
|
161
|
+
* never carries an unresolved `phis:` URI in the field where a literal URL belongs.
|
|
162
|
+
*/
|
|
163
|
+
const PHI_STORABLE_EXTERNAL_HREF_PATTERN = /^(?:https?:\/\/|\/\/|mailto:|tel:|#)/iu;
|
|
164
|
+
export function isPhiStorableExternalHref(value) {
|
|
165
|
+
const trimmed = value.trim();
|
|
166
|
+
return trimmed.length > 0
|
|
167
|
+
&& !trimmed.toLowerCase().startsWith("phis:")
|
|
168
|
+
&& PHI_STORABLE_EXTERNAL_HREF_PATTERN.test(trimmed);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A stored link target, read back under the contract rather than trusted.
|
|
172
|
+
*
|
|
173
|
+
* Anything that is not one of the two shapes answers `null`, and a Control that gets `null` draws no
|
|
174
|
+
* link at all. That is the same answer the contract gives for a Page reference that no longer resolves:
|
|
175
|
+
* non-interactive text beats a link that goes somewhere nobody chose.
|
|
176
|
+
*/
|
|
177
|
+
export function readPhiLinkTarget(value) {
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const record = value;
|
|
182
|
+
const newTab = record.newTab === true ? { newTab: true } : {};
|
|
183
|
+
if (record.kind === "page") {
|
|
184
|
+
const parsed = readPhiPageReference(record.reference);
|
|
185
|
+
if (!parsed) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const fragment = typeof record.fragment === "string" ? record.fragment.replace(/^#/u, "").trim() : "";
|
|
189
|
+
/*
|
|
190
|
+
* An Area that is not one is dropped rather than kept: the alternative is a stored value that asks
|
|
191
|
+
* a question nowhere can answer, and a link drawing nothing with no way to see why. Falling back to
|
|
192
|
+
* the asking Area is the behaviour of every link that names none, which is the honest default.
|
|
193
|
+
*/
|
|
194
|
+
const area = isPhiCmsAreaKey(record.area) ? { area: record.area } : {};
|
|
195
|
+
return {
|
|
196
|
+
kind: "page",
|
|
197
|
+
reference: parsed.reference,
|
|
198
|
+
...area,
|
|
199
|
+
...(fragment ? { fragment } : {}),
|
|
200
|
+
...newTab,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (record.kind === "external") {
|
|
204
|
+
const href = typeof record.href === "string" ? record.href.trim() : "";
|
|
205
|
+
return isPhiStorableExternalHref(href) ? { kind: "external", href, ...newTab } : null;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* References from a Phi record to something in another system: `@phis/server/resource-links:v1`.
|
|
3
|
+
*
|
|
4
|
+
* A ticket that is also a GitHub issue, a task that is also a pull request, a thread that is also a
|
|
5
|
+
* conversation somewhere else. The reference is a row of its own rather than a column on the record,
|
|
6
|
+
* because a record may have several at once, across several systems, and a column per system is a
|
|
7
|
+
* schema change per integration.
|
|
8
|
+
*
|
|
9
|
+
* The provider is the Add-on that wrote the link, and it is the whole of the ownership rule: an Add-on
|
|
10
|
+
* reads and writes its own links and sees no others. Two integrations pointing at one ticket therefore
|
|
11
|
+
* do not have to know about each other, and removing one takes its references with it.
|
|
12
|
+
*
|
|
13
|
+
* The link is also what an Add-on's actorless access is made of. A person who can see a record writes
|
|
14
|
+
* the link; from then on the Add-on may read and append to the conversation that record carries, and
|
|
15
|
+
* nothing beyond it. Unlinking ends that -- which is why `Synced` is stated per link rather than
|
|
16
|
+
* assumed: a reference that is only a pointer should not become a standing permission.
|
|
17
|
+
*/
|
|
18
|
+
/** What a link points away from. */
|
|
19
|
+
export type PhisResourceLinkSubject = {
|
|
20
|
+
kind: "thread";
|
|
21
|
+
id: number;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "ticket";
|
|
24
|
+
id: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "ticketTask";
|
|
27
|
+
id: number;
|
|
28
|
+
};
|
|
29
|
+
/** What is true of a link, as bit flags. */
|
|
30
|
+
export declare const PhisResourceLinkFlag: {
|
|
31
|
+
/**
|
|
32
|
+
* The two sides are kept in step, and this Add-on may reach the subject's thread without an actor.
|
|
33
|
+
*
|
|
34
|
+
* A link without it is a reference someone recorded: it is shown, it is followed, and it grants
|
|
35
|
+
* nothing.
|
|
36
|
+
*/
|
|
37
|
+
readonly Synced: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One reference, as Core stores it.
|
|
41
|
+
*
|
|
42
|
+
* `type` and `state` are the provider's own vocabulary -- an issue, a pull request, open, merged -- and
|
|
43
|
+
* Core never interprets either. It stores them, indexes the external id against them, and hands them
|
|
44
|
+
* back; what `21` means is the Add-on's business, exactly as a role name is.
|
|
45
|
+
*/
|
|
46
|
+
export type PhisResourceLink = {
|
|
47
|
+
id: number;
|
|
48
|
+
subject: PhisResourceLinkSubject;
|
|
49
|
+
type: number;
|
|
50
|
+
externalId: string;
|
|
51
|
+
url: string | null;
|
|
52
|
+
state: number | null;
|
|
53
|
+
flags: number;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The resource links capability: `@phis/server/resource-links:v1`.
|
|
59
|
+
*
|
|
60
|
+
* Bounded twice: to this Add-on's own links, and to subjects the acting user may reach. Writing needs
|
|
61
|
+
* an actor -- a link is a person saying these two things belong together, and it is what later lets the
|
|
62
|
+
* Add-on act with no person there. A hook or a job may read the links it wrote and may not make new
|
|
63
|
+
* ones.
|
|
64
|
+
*/
|
|
65
|
+
export type PhisResourceLinksCapabilityV1 = {
|
|
66
|
+
/** This Add-on's links on one subject, or none where the subject is out of reach. */
|
|
67
|
+
list(subject: PhisResourceLinkSubject): Promise<PhisResourceLink[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Records a reference, or hands back the one that is already there.
|
|
70
|
+
*
|
|
71
|
+
* Asking twice is how an id is learned, the way a declared group is: `(type, externalId)` is unique
|
|
72
|
+
* within this Add-on and this Site, so a second call answers with the first link instead of failing.
|
|
73
|
+
*/
|
|
74
|
+
link(input: {
|
|
75
|
+
subject: PhisResourceLinkSubject;
|
|
76
|
+
type: number;
|
|
77
|
+
externalId: string;
|
|
78
|
+
url?: string | null;
|
|
79
|
+
state?: number | null;
|
|
80
|
+
flags?: number;
|
|
81
|
+
}): Promise<PhisResourceLink>;
|
|
82
|
+
/** Moves a link's provider-local state. */
|
|
83
|
+
setState(input: {
|
|
84
|
+
linkId: number;
|
|
85
|
+
state: number | null;
|
|
86
|
+
}): Promise<boolean>;
|
|
87
|
+
/** Removes a link, and with it whatever actorless reach it granted. */
|
|
88
|
+
unlink(linkId: number): Promise<boolean>;
|
|
89
|
+
/** One link of this Add-on by the external id it recorded, or null. */
|
|
90
|
+
findByExternalId(input: {
|
|
91
|
+
type: number;
|
|
92
|
+
externalId: string;
|
|
93
|
+
}): Promise<PhisResourceLink | null>;
|
|
94
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* References from a Phi record to something in another system: `@phis/server/resource-links:v1`.
|
|
3
|
+
*
|
|
4
|
+
* A ticket that is also a GitHub issue, a task that is also a pull request, a thread that is also a
|
|
5
|
+
* conversation somewhere else. The reference is a row of its own rather than a column on the record,
|
|
6
|
+
* because a record may have several at once, across several systems, and a column per system is a
|
|
7
|
+
* schema change per integration.
|
|
8
|
+
*
|
|
9
|
+
* The provider is the Add-on that wrote the link, and it is the whole of the ownership rule: an Add-on
|
|
10
|
+
* reads and writes its own links and sees no others. Two integrations pointing at one ticket therefore
|
|
11
|
+
* do not have to know about each other, and removing one takes its references with it.
|
|
12
|
+
*
|
|
13
|
+
* The link is also what an Add-on's actorless access is made of. A person who can see a record writes
|
|
14
|
+
* the link; from then on the Add-on may read and append to the conversation that record carries, and
|
|
15
|
+
* nothing beyond it. Unlinking ends that -- which is why `Synced` is stated per link rather than
|
|
16
|
+
* assumed: a reference that is only a pointer should not become a standing permission.
|
|
17
|
+
*/
|
|
18
|
+
/** What is true of a link, as bit flags. */
|
|
19
|
+
export const PhisResourceLinkFlag = {
|
|
20
|
+
/**
|
|
21
|
+
* The two sides are kept in step, and this Add-on may reach the subject's thread without an actor.
|
|
22
|
+
*
|
|
23
|
+
* A link without it is a reference someone recorded: it is shown, it is followed, and it grants
|
|
24
|
+
* nothing.
|
|
25
|
+
*/
|
|
26
|
+
Synced: 1 << 0,
|
|
27
|
+
};
|
package/dist/signals.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export declare function isPhiSignalAddressSegment(value: string): boolean;
|
|
|
78
78
|
*/
|
|
79
79
|
export declare function isPhiControllerPluginKey(value: string): boolean;
|
|
80
80
|
export declare function isPhiControllerSignalAddress(address: unknown): address is PhiControllerSignalAddress;
|
|
81
|
+
export declare function readPhiControllerSignalAddress(value: unknown): PhiControllerSignalAddress | undefined;
|
|
81
82
|
/** The instance a `cms:` address names, or null when the string is not one. */
|
|
82
83
|
export declare function readPhiCmsSignalAddressInstanceId(address: string): PhiCmsInstanceId | null;
|
|
83
84
|
export declare function isPhiRegionSignalAddress(address: string): boolean;
|
package/dist/signals.js
CHANGED
|
@@ -146,6 +146,9 @@ export function isPhiControllerSignalAddress(address) {
|
|
|
146
146
|
isPhiSignalAddressSegment(namespacedType.slice(controllerSeparatorIndex + 1)) &&
|
|
147
147
|
isPhiSignalAddressSegment(instanceKey));
|
|
148
148
|
}
|
|
149
|
+
export function readPhiControllerSignalAddress(value) {
|
|
150
|
+
return isPhiControllerSignalAddress(value) ? value.trim() : undefined;
|
|
151
|
+
}
|
|
149
152
|
/** The instance a `cms:` address names, or null when the string is not one. */
|
|
150
153
|
export function readPhiCmsSignalAddressInstanceId(address) {
|
|
151
154
|
if (!address.startsWith("cms:")) {
|
package/dist/site-groups.d.ts
CHANGED
|
@@ -36,3 +36,18 @@ export declare const PHI_GROUP_MEMBERSHIP_LEVELS: readonly [1, number, number, n
|
|
|
36
36
|
* Reading downwards is what makes an unknown higher bit fail closed rather than round up.
|
|
37
37
|
*/
|
|
38
38
|
export declare function readPhiGroupMembershipLevel(value: unknown): PhiGroupMembershipFlagValue | null;
|
|
39
|
+
/**
|
|
40
|
+
* A group a Module needs the Site to have, named rather than created.
|
|
41
|
+
*
|
|
42
|
+
* It travels in a Module's `seed` and is written when the Area carrying that Module is published. The
|
|
43
|
+
* key is the Module's own and never changes: it is what makes a second publish find the group it made
|
|
44
|
+
* the first time instead of making another. `provider_id` is the Module's id and is supplied by the
|
|
45
|
+
* materialization, not by the declaration, so a Module cannot claim a group belonging to another.
|
|
46
|
+
*
|
|
47
|
+
* The name is a starting value. Once the row exists it is the Site's, and administration renames it
|
|
48
|
+
* without the next publish undoing that.
|
|
49
|
+
*/
|
|
50
|
+
export type PhisDeclaredSiteGroup = {
|
|
51
|
+
readonly key: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
};
|