@segmentstream-cloud/ontology 0.7.0 → 0.9.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/README.md +15 -8
- package/dist/analyze.js +12 -4
- package/dist/authoring-input.d.ts +3 -0
- package/dist/authoring-input.js +107 -0
- package/dist/authoring-names.d.ts +4 -0
- package/dist/authoring-names.js +13 -0
- package/dist/authoring-records.d.ts +16 -0
- package/dist/authoring-records.js +144 -0
- package/dist/authoring.d.ts +12 -0
- package/dist/authoring.js +237 -0
- package/dist/entity.d.ts +7 -8
- package/dist/entity.js +31 -8
- package/dist/migrate/plan.js +5 -4
- package/dist/names.d.ts +5 -2
- package/dist/names.js +27 -0
- package/dist/permissions.js +5 -5
- package/dist/property.js +2 -1
- package/dist/runtime/generated-actions.js +1 -2
- package/dist/runtime/principals.js +2 -2
- package/dist/service/authentication.d.ts +33 -4
- package/dist/service/authentication.js +142 -6
- package/dist/service/config.d.ts +1 -1
- package/dist/service/docs.d.ts +9 -0
- package/dist/service/docs.js +197 -0
- package/dist/service/errors.d.ts +5 -0
- package/dist/service/errors.js +14 -0
- package/dist/service/handlers.js +25 -16
- package/dist/service/service.js +14 -5
- package/dist/user.d.ts +3 -3
- package/dist/user.js +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
|
|
3
|
+
import { BASE_PATH } from "./api.js";
|
|
4
|
+
/** The API reference page: `GET /api/ontology/docs`. */
|
|
5
|
+
export const DOCS_PATH = `${BASE_PATH}/docs`;
|
|
6
|
+
const OPENAPI_PATH = `${BASE_PATH}/openapi.json`;
|
|
7
|
+
/**
|
|
8
|
+
* Scalar's API reference, pinned with its integrity hash: a different file never runs. Bump both
|
|
9
|
+
* together: `curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A`.
|
|
10
|
+
*/
|
|
11
|
+
const SCALAR_SCRIPT = {
|
|
12
|
+
src: "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.73.1/dist/browser/standalone.js",
|
|
13
|
+
integrity: "sha384-kYDGzV91Jnn3TbHINV3nt54riK2uMJDfN5Al8dAkz4FssELTBWbD8rgw32sTKfOi",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Scalar without its hosted extras: no telemetry, AI agent, MCP button, developer tools or fonts
|
|
17
|
+
* from scalar.com. Requests from the page go to the ontology itself, never through Scalar's proxy.
|
|
18
|
+
*/
|
|
19
|
+
const SCALAR_CONFIGURATION = {
|
|
20
|
+
telemetry: false,
|
|
21
|
+
agent: { disabled: true },
|
|
22
|
+
mcp: { disabled: true },
|
|
23
|
+
showDeveloperTools: "never",
|
|
24
|
+
withDefaultFonts: false,
|
|
25
|
+
hideClientButton: true,
|
|
26
|
+
documentDownloadType: "json",
|
|
27
|
+
};
|
|
28
|
+
/** The key the page keeps a pasted token under, for this tab only. */
|
|
29
|
+
const TOKEN_STORAGE_KEY = "segmentstream-ontology-docs-token";
|
|
30
|
+
/** JSON inside a `<script>`: nothing in it can close the element. */
|
|
31
|
+
function scriptJson(value) {
|
|
32
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
33
|
+
}
|
|
34
|
+
function contentSecurityPolicy(nonce) {
|
|
35
|
+
return [
|
|
36
|
+
"default-src 'none'",
|
|
37
|
+
`script-src 'nonce-${nonce}' https://cdn.jsdelivr.net`,
|
|
38
|
+
// Scalar injects its styles at runtime.
|
|
39
|
+
"style-src 'self' 'unsafe-inline'",
|
|
40
|
+
"img-src 'self' data: blob: https:",
|
|
41
|
+
"font-src 'self' data:",
|
|
42
|
+
"connect-src 'self'",
|
|
43
|
+
"worker-src 'self' blob:",
|
|
44
|
+
"base-uri 'none'",
|
|
45
|
+
"form-action 'none'",
|
|
46
|
+
"frame-ancestors 'none'",
|
|
47
|
+
].join("; ");
|
|
48
|
+
}
|
|
49
|
+
function page(nonce) {
|
|
50
|
+
const settings = scriptJson({
|
|
51
|
+
openapi: OPENAPI_PATH,
|
|
52
|
+
storageKey: TOKEN_STORAGE_KEY,
|
|
53
|
+
configuration: SCALAR_CONFIGURATION,
|
|
54
|
+
});
|
|
55
|
+
return `<!doctype html>
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="utf-8" />
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
60
|
+
<meta name="robots" content="noindex" />
|
|
61
|
+
<title>Ontology API</title>
|
|
62
|
+
<style>
|
|
63
|
+
#sign-in {
|
|
64
|
+
font: 14px/1.5 system-ui, sans-serif;
|
|
65
|
+
max-width: 28rem;
|
|
66
|
+
margin: 15vh auto;
|
|
67
|
+
padding: 0 1.5rem;
|
|
68
|
+
color: #18181b;
|
|
69
|
+
}
|
|
70
|
+
#sign-in h1 { font-size: 1.25rem; margin: 0 0 .5rem; }
|
|
71
|
+
#sign-in p { margin: 0 0 1rem; color: #52525b; }
|
|
72
|
+
#sign-in label { display: block; font-weight: 500; margin-bottom: .25rem; }
|
|
73
|
+
#sign-in input {
|
|
74
|
+
box-sizing: border-box;
|
|
75
|
+
width: 100%;
|
|
76
|
+
padding: .5rem .625rem;
|
|
77
|
+
border: 1px solid #d4d4d8;
|
|
78
|
+
border-radius: .375rem;
|
|
79
|
+
font: inherit;
|
|
80
|
+
}
|
|
81
|
+
#sign-in button {
|
|
82
|
+
margin-top: .75rem;
|
|
83
|
+
padding: .5rem .875rem;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: .375rem;
|
|
86
|
+
background: #18181b;
|
|
87
|
+
color: #fff;
|
|
88
|
+
font: inherit;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
}
|
|
91
|
+
#sign-in [role="alert"] { margin-top: .75rem; color: #b91c1c; }
|
|
92
|
+
</style>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<main id="sign-in" hidden>
|
|
96
|
+
<form>
|
|
97
|
+
<h1>Ontology API</h1>
|
|
98
|
+
<p>Sign in to the app in this browser, or paste a bearer token or an API key (ssk_…).</p>
|
|
99
|
+
<label for="token">Token or API key</label>
|
|
100
|
+
<input id="token" type="password" autocomplete="off" spellcheck="false" required />
|
|
101
|
+
<button type="submit">Open the reference</button>
|
|
102
|
+
<p role="alert"></p>
|
|
103
|
+
</form>
|
|
104
|
+
</main>
|
|
105
|
+
<div id="app"></div>
|
|
106
|
+
<script
|
|
107
|
+
nonce="${nonce}"
|
|
108
|
+
src="${SCALAR_SCRIPT.src}"
|
|
109
|
+
integrity="${SCALAR_SCRIPT.integrity}"
|
|
110
|
+
crossorigin="anonymous"
|
|
111
|
+
></script>
|
|
112
|
+
<script nonce="${nonce}">
|
|
113
|
+
(() => {
|
|
114
|
+
const settings = ${settings};
|
|
115
|
+
const signIn = document.getElementById("sign-in");
|
|
116
|
+
const form = signIn.querySelector("form");
|
|
117
|
+
const input = document.getElementById("token");
|
|
118
|
+
const alert = signIn.querySelector("[role=alert]");
|
|
119
|
+
|
|
120
|
+
async function load(token) {
|
|
121
|
+
const response = await fetch(settings.openapi, {
|
|
122
|
+
credentials: "same-origin",
|
|
123
|
+
headers: token ? { authorization: "Bearer " + token } : {},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
return { spec: await response.json() };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const body = await response.json().catch(() => ({}));
|
|
131
|
+
|
|
132
|
+
return { status: response.status, detail: body.detail };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function show(spec, token) {
|
|
136
|
+
signIn.hidden = true;
|
|
137
|
+
window.Scalar.createApiReference(document.getElementById("app"), {
|
|
138
|
+
...settings.configuration,
|
|
139
|
+
content: spec,
|
|
140
|
+
authentication: {
|
|
141
|
+
preferredSecurityScheme: "bearer",
|
|
142
|
+
...(token ? { securitySchemes: { bearer: { token } } } : {}),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function open(token) {
|
|
148
|
+
let result;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
result = await load(token);
|
|
152
|
+
} catch {
|
|
153
|
+
result = { status: 0 };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.spec !== undefined) {
|
|
157
|
+
if (token) {
|
|
158
|
+
sessionStorage.setItem(settings.storageKey, token);
|
|
159
|
+
}
|
|
160
|
+
show(result.spec, token);
|
|
161
|
+
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
sessionStorage.removeItem(settings.storageKey);
|
|
166
|
+
signIn.hidden = false;
|
|
167
|
+
alert.textContent =
|
|
168
|
+
!token && result.status === 401
|
|
169
|
+
? ""
|
|
170
|
+
: (result.detail ?? "The reference could not be loaded. Try again shortly.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
form.addEventListener("submit", (event) => {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
open(input.value.trim());
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
open(sessionStorage.getItem(settings.storageKey) ?? "");
|
|
179
|
+
})();
|
|
180
|
+
</script>
|
|
181
|
+
</body>
|
|
182
|
+
</html>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The API reference at `/api/ontology/docs`: Scalar over the service's own `openapi.json`. The page
|
|
187
|
+
* is public and holds no part of the ontology: it loads the document as the reader, with the app's
|
|
188
|
+
* session cookie when they are signed in to the app, or with a token they paste.
|
|
189
|
+
*/
|
|
190
|
+
export const DocsRoute = HttpRouter.add("GET", DOCS_PATH, Effect.sync(() => {
|
|
191
|
+
const nonce = crypto.randomUUID().replaceAll("-", "");
|
|
192
|
+
return HttpServerResponse.html(page(nonce)).pipe(HttpServerResponse.setHeaders({
|
|
193
|
+
"content-security-policy": contentSecurityPolicy(nonce),
|
|
194
|
+
"referrer-policy": "no-referrer",
|
|
195
|
+
"x-content-type-options": "nosniff",
|
|
196
|
+
}));
|
|
197
|
+
}));
|
package/dist/service/errors.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export declare function unauthorized(requestId: string, detail: string): ErrorRe
|
|
|
51
51
|
* The detail is written for the person, since the app shows it.
|
|
52
52
|
*/
|
|
53
53
|
export declare function notInvited(requestId: string): ErrorResponse;
|
|
54
|
+
/**
|
|
55
|
+
* A request that changes something came with the session cookie but not from the app's own pages.
|
|
56
|
+
* The cookie rides along with any request to the app's domain, so only its origin can vouch for it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function crossOriginSession(requestId: string): ErrorResponse;
|
|
54
59
|
export declare function unavailable(requestId: string): ErrorResponse;
|
|
55
60
|
export declare function internalError(requestId: string): ErrorResponse;
|
|
56
61
|
export declare function routeNotFound(requestId: string): ErrorResponse;
|
package/dist/service/errors.js
CHANGED
|
@@ -67,6 +67,20 @@ export function notInvited(requestId) {
|
|
|
67
67
|
body: { error: "not_invited", detail: "Ask your administrator for an invitation.", requestId },
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* A request that changes something came with the session cookie but not from the app's own pages.
|
|
72
|
+
* The cookie rides along with any request to the app's domain, so only its origin can vouch for it.
|
|
73
|
+
*/
|
|
74
|
+
export function crossOriginSession(requestId) {
|
|
75
|
+
return {
|
|
76
|
+
status: 403,
|
|
77
|
+
body: {
|
|
78
|
+
error: "forbidden",
|
|
79
|
+
detail: "The session cookie only authenticates requests from the app's own pages; send a bearer token.",
|
|
80
|
+
requestId,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
70
84
|
export function unavailable(requestId) {
|
|
71
85
|
return {
|
|
72
86
|
status: 503,
|
package/dist/service/handlers.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HttpServerResponse } from "effect/unstable/http";
|
|
2
2
|
import { Effect, Layer, Schema } from "effect";
|
|
3
3
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
4
|
+
import { editsFromWire, generatedResultFromWire, recordFromWire } from "../authoring-records.js";
|
|
5
|
+
import { nameToWire } from "../authoring-names.js";
|
|
6
|
+
import { principalFromWire } from "../authoring.js";
|
|
7
|
+
import { toCamelCase } from "../names.js";
|
|
4
8
|
import { PermissionDenied } from "../errors.js";
|
|
5
9
|
import { missingPermission, permissionOf, READ_VERB } from "../permissions.js";
|
|
6
10
|
import { EMAIL_PATTERN } from "../property.js";
|
|
@@ -141,26 +145,31 @@ function readEnvelope(request) {
|
|
|
141
145
|
* The configured hook as `runAction` runs it after the commit. A failure is logged with the request
|
|
142
146
|
* id here, so the action's caller still gets the committed result.
|
|
143
147
|
*/
|
|
144
|
-
function afterCommitOf(hook, request) {
|
|
145
|
-
return (committed) =>
|
|
146
|
-
entity
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
function afterCommitOf(hook, request, ontology) {
|
|
149
|
+
return (committed) => {
|
|
150
|
+
const definition = ontology.config.entities.find((entity) => nameToWire(entity.name) === committed.entity)?.definition;
|
|
151
|
+
return Effect.tryPromise(async () => hook({
|
|
152
|
+
entity: toCamelCase(committed.entity),
|
|
153
|
+
action: toCamelCase(committed.action),
|
|
154
|
+
principal: principalFromWire(committed.principal),
|
|
155
|
+
input: request.generated ? recordFromWire(committed.input, definition) : committed.input,
|
|
156
|
+
result: request.generated
|
|
157
|
+
? generatedResultFromWire(committed.result, committed.action, definition)
|
|
158
|
+
: committed.result,
|
|
159
|
+
edits: editsFromWire(committed.edits, ontology.config.entities),
|
|
160
|
+
requestId: request.requestId,
|
|
161
|
+
})).pipe(Effect.catchCause((cause) => Effect.logError("ontology.after_commit.failed", cause).pipe(Effect.annotateLogs({
|
|
162
|
+
requestId: request.requestId,
|
|
163
|
+
action: `${committed.entity}.${committed.action}`,
|
|
164
|
+
}))));
|
|
165
|
+
};
|
|
157
166
|
}
|
|
158
167
|
/** The deployment's OIDC token: Vercel sets it on each request, and in the environment of builds. */
|
|
159
168
|
function identityToken(request) {
|
|
160
169
|
const header = request.headers[OIDC_TOKEN_HEADER];
|
|
161
170
|
return typeof header === "string" && header !== "" ? header : process.env.VERCEL_OIDC_TOKEN;
|
|
162
171
|
}
|
|
163
|
-
function runAction(planned, { name }, hook, connections) {
|
|
172
|
+
function runAction(planned, { name, generated }, ontology, hook, connections) {
|
|
164
173
|
return ({ request }) => Effect.gen(function* () {
|
|
165
174
|
const backend = yield* OntologyBackend;
|
|
166
175
|
const principal = yield* CurrentPrincipal;
|
|
@@ -180,7 +189,7 @@ function runAction(planned, { name }, hook, connections) {
|
|
|
180
189
|
}),
|
|
181
190
|
...(hook === undefined
|
|
182
191
|
? {}
|
|
183
|
-
: { afterCommit: afterCommitOf(hook, {
|
|
192
|
+
: { afterCommit: afterCommitOf(hook, { requestId, generated }, ontology) }),
|
|
184
193
|
});
|
|
185
194
|
const { issues } = outcome.validation;
|
|
186
195
|
if (!outcome.validation.ok && !validateOnly) {
|
|
@@ -214,7 +223,7 @@ function entityHandlers(built, planned, hooks, targets, connections) {
|
|
|
214
223
|
registered = registered.handle(`related.${traversal.name}`, (request) => traverse(planned, traversal, request, targets).pipe(Effect.provideService(OntologyBackend, backend)));
|
|
215
224
|
}
|
|
216
225
|
for (const action of planned.actions) {
|
|
217
|
-
const handler = runAction(planned, action, hooks.get(`${planned.name}.${action.name}`), connections);
|
|
226
|
+
const handler = runAction(planned, action, built.plan.ontology, hooks.get(`${planned.name}.${action.name}`), connections);
|
|
218
227
|
registered = registered.handleRaw(`actions.${action.name}`, (request) => handler(request).pipe(Effect.provideService(OntologyBackend, backend)));
|
|
219
228
|
}
|
|
220
229
|
return registered;
|
package/dist/service/service.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Layer } from "effect";
|
|
2
2
|
import { HttpRouter, HttpServer, HttpServerError } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
4
|
+
import { nameToWire } from "../authoring-names.js";
|
|
4
5
|
import { sqlServiceRuntime } from "../runtime/service-binding.js";
|
|
5
6
|
import { ErrorResponsesLive, buildApi } from "./api.js";
|
|
6
|
-
import { authenticationLayer, tokenVerifier } from "./authentication.js";
|
|
7
|
+
import { authenticationLayer, neonAuthSessionExchange, tokenVerifier } from "./authentication.js";
|
|
7
8
|
import { OntologyServiceConfigError, } from "./config.js";
|
|
8
9
|
import { RequestId, failureResponse, internalError, errorBodyForWire, routeNotFound, toServerResponse, } from "./errors.js";
|
|
9
10
|
import { deployedConnections, localConnections } from "./connections.js";
|
|
11
|
+
import { DocsRoute } from "./docs.js";
|
|
10
12
|
import { handlersLayer } from "./handlers.js";
|
|
11
13
|
import { planApi } from "./plan.js";
|
|
12
14
|
/** The header every response carries: the hash of the manifest the service serves. */
|
|
@@ -15,13 +17,19 @@ export const ONTOLOGY_HASH_HEADER = "x-ontology-hash";
|
|
|
15
17
|
export const REQUEST_ID_HEADER = "x-request-id";
|
|
16
18
|
function afterCommitHooks(plan, hooks = {}) {
|
|
17
19
|
const actions = new Set(plan.entities.flatMap(({ name, actions: planned }) => planned.map((action) => `${name}.${action.name}`)));
|
|
18
|
-
const
|
|
20
|
+
const entries = Object.entries(hooks).map(([key, hook]) => [key.split(".").map(nameToWire).join("."), hook]);
|
|
21
|
+
const unknown = Object.keys(hooks).filter((key) => !actions.has(key.split(".").map(nameToWire).join(".")));
|
|
19
22
|
if (unknown.length > 0) {
|
|
20
23
|
throw new OntologyServiceConfigError({
|
|
21
24
|
message: `afterCommit names actions the ontology does not have: ${unknown.join(", ")}.`,
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
|
-
|
|
27
|
+
if (new Set(entries.map(([key]) => key)).size !== entries.length) {
|
|
28
|
+
throw new OntologyServiceConfigError({
|
|
29
|
+
message: "afterCommit names collide after snake_case conversion.",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return new Map(entries);
|
|
25
33
|
}
|
|
26
34
|
/**
|
|
27
35
|
* Requests that match no route, and defects that escape a route's own error handling, still get a
|
|
@@ -55,13 +63,14 @@ export function createOntologyService(ontology, config) {
|
|
|
55
63
|
const built = buildApi(plan);
|
|
56
64
|
const hooks = afterCommitHooks(plan, config.afterCommit);
|
|
57
65
|
const verifyToken = tokenVerifier(config);
|
|
66
|
+
const exchangeSession = config.neonAuth === undefined ? undefined : neonAuthSessionExchange(config.neonAuth);
|
|
58
67
|
const backend = runtime.connect({ ontology, databaseUrl: config.databaseUrl });
|
|
59
68
|
const connections = config.connections ??
|
|
60
69
|
(config.localConnections === undefined
|
|
61
70
|
? deployedConnections(config)
|
|
62
71
|
: localConnections(config.localConnections));
|
|
63
|
-
const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections)), Layer.provide([authenticationLayer(verifyToken), ErrorResponsesLive]), Layer.provide(backend));
|
|
64
|
-
const application = Layer.mergeAll(routes, UnmatchedRoutes).pipe(Layer.provide(HttpServer.layerServices));
|
|
72
|
+
const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections)), Layer.provide([authenticationLayer(verifyToken, exchangeSession), ErrorResponsesLive]), Layer.provide(backend));
|
|
73
|
+
const application = Layer.mergeAll(routes, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(HttpServer.layerServices));
|
|
65
74
|
const web = HttpRouter.toWebHandler(application, { disableLogger: true });
|
|
66
75
|
const hash = ontology.manifest.hash;
|
|
67
76
|
return {
|
package/dist/user.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export declare const User: import("./entity.js").DraftEntity<"user", {
|
|
|
13
13
|
readonly email: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, true>>;
|
|
14
14
|
readonly kind: import("./property.js").Property<"member" | "service", false, false>;
|
|
15
15
|
readonly active: import("./property.js").Property<boolean, true, false>;
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
16
|
+
readonly identityProvider: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
17
|
+
readonly identitySubject: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
18
|
+
readonly lastSignInAt: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
19
19
|
}, import("./entity.js").Relationships>;
|
|
20
20
|
export type User = typeof User.Type;
|
package/dist/user.js
CHANGED
|
@@ -41,13 +41,13 @@ export const User = entity("user", {
|
|
|
41
41
|
managed: true,
|
|
42
42
|
description: "Inactive users cannot sign in or run actions.",
|
|
43
43
|
}),
|
|
44
|
-
|
|
44
|
+
identityProvider: p.text({
|
|
45
45
|
renamedFrom: "identityProvider",
|
|
46
46
|
optional: true,
|
|
47
47
|
managed: true,
|
|
48
48
|
description: "Who signs the member in, for example neon-auth. Empty for services.",
|
|
49
49
|
}),
|
|
50
|
-
|
|
50
|
+
identitySubject: p.text({
|
|
51
51
|
renamedFrom: "identitySubject",
|
|
52
52
|
optional: true,
|
|
53
53
|
managed: true,
|
|
@@ -55,7 +55,7 @@ export const User = entity("user", {
|
|
|
55
55
|
}),
|
|
56
56
|
// Optional so that a table created before it gains the column without a backfill: a member
|
|
57
57
|
// who has not signed in since simply has none.
|
|
58
|
-
|
|
58
|
+
lastSignInAt: p.timestamp({
|
|
59
59
|
renamedFrom: "lastSignInAt",
|
|
60
60
|
optional: true,
|
|
61
61
|
managed: true,
|
|
@@ -63,5 +63,5 @@ export const User = entity("user", {
|
|
|
63
63
|
description: "When the member last signed in to the app. Empty until they do.",
|
|
64
64
|
}),
|
|
65
65
|
},
|
|
66
|
-
indexes: [{ properties: ["
|
|
66
|
+
indexes: [{ properties: ["identityProvider", "identitySubject"], unique: true }],
|
|
67
67
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/ontology",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|