@choiceapps/communityservices-sdk 0.1.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/AGENTS.md +43 -0
- package/API.md +86 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +100 -0
- package/SSVF.md +131 -0
- package/browser/attachment-conflict.mjs +6 -0
- package/browser/case-plans-ui.mjs +23 -0
- package/browser/case-plans.d.mts +20 -0
- package/browser/case-plans.mjs +89 -0
- package/browser/client-creation.d.mts +16 -0
- package/browser/client-creation.mjs +60 -0
- package/browser/client-profile-ui.mjs +59 -0
- package/browser/client-profile.d.mts +24 -0
- package/browser/client-profile.mjs +94 -0
- package/browser/client-workflows.mjs +135 -0
- package/browser/connection.d.mts +4 -0
- package/browser/connection.mjs +41 -0
- package/browser/current-living-situation.d.mts +44 -0
- package/browser/current-living-situation.mjs +161 -0
- package/browser/date-of-birth-ui.mjs +68 -0
- package/browser/date-of-birth.d.mts +28 -0
- package/browser/date-of-birth.mjs +112 -0
- package/browser/enrollment-workflow.d.mts +174 -0
- package/browser/enrollment-workflow.mjs +518 -0
- package/browser/exit-details.mjs +21 -0
- package/browser/file-store.mjs +32 -0
- package/browser/gwt-mapped.mjs +46 -0
- package/browser/household-exit.d.mts +34 -0
- package/browser/household-exit.mjs +102 -0
- package/browser/households-ui.mjs +20 -0
- package/browser/households.d.mts +22 -0
- package/browser/households.mjs +115 -0
- package/browser/index.d.mts +78 -0
- package/browser/index.mjs +46 -0
- package/browser/member-entry-date.d.mts +30 -0
- package/browser/member-entry-date.mjs +71 -0
- package/browser/need-recording.d.mts +37 -0
- package/browser/need-recording.mjs +68 -0
- package/browser/need-ui.mjs +138 -0
- package/browser/page-lock.mjs +11 -0
- package/browser/password-recovery.mjs +49 -0
- package/browser/playwright-session.mjs +196 -0
- package/browser/program-discovery.d.mts +22 -0
- package/browser/program-discovery.mjs +80 -0
- package/browser/recovery-diagnostics.mjs +9 -0
- package/browser/service-recording.d.mts +45 -0
- package/browser/service-recording.mjs +148 -0
- package/browser/service-ui.mjs +160 -0
- package/browser/session-authentication.mjs +40 -0
- package/browser/shelter-inventory.d.mts +103 -0
- package/browser/shelter-inventory.mjs +273 -0
- package/browser/shelter-ui.mjs +382 -0
- package/browser/shelters.d.mts +139 -0
- package/browser/shelters.mjs +425 -0
- package/browser/ssvf-rules.d.mts +21 -0
- package/browser/ssvf-rules.mjs +145 -0
- package/browser/ui-login.mjs +135 -0
- package/dist/cjs/bulk.d.ts +33 -0
- package/dist/cjs/bulk.d.ts.map +1 -0
- package/dist/cjs/bulk.js +42 -0
- package/dist/cjs/bulk.js.map +1 -0
- package/dist/cjs/control.d.ts +35 -0
- package/dist/cjs/control.d.ts.map +1 -0
- package/dist/cjs/control.js +141 -0
- package/dist/cjs/control.js.map +1 -0
- package/dist/cjs/effective-date.d.ts +3 -0
- package/dist/cjs/effective-date.d.ts.map +1 -0
- package/dist/cjs/effective-date.js +27 -0
- package/dist/cjs/effective-date.js.map +1 -0
- package/dist/cjs/encoding.d.ts +32 -0
- package/dist/cjs/encoding.d.ts.map +1 -0
- package/dist/cjs/encoding.js +121 -0
- package/dist/cjs/encoding.js.map +1 -0
- package/dist/cjs/enrollment-verifications.d.ts +39 -0
- package/dist/cjs/enrollment-verifications.d.ts.map +1 -0
- package/dist/cjs/enrollment-verifications.js +96 -0
- package/dist/cjs/enrollment-verifications.js.map +1 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/generated/v2-models.d.ts +1482 -0
- package/dist/cjs/generated/v2-models.d.ts.map +1 -0
- package/dist/cjs/generated/v2-models.js +3 -0
- package/dist/cjs/generated/v2-models.js.map +1 -0
- package/dist/cjs/generated/v2.d.ts +1149 -0
- package/dist/cjs/generated/v2.d.ts.map +1 -0
- package/dist/cjs/generated/v2.js +252 -0
- package/dist/cjs/generated/v2.js.map +1 -0
- package/dist/cjs/generated/v3-models.d.ts +1420 -0
- package/dist/cjs/generated/v3-models.d.ts.map +1 -0
- package/dist/cjs/generated/v3-models.js +3 -0
- package/dist/cjs/generated/v3-models.js.map +1 -0
- package/dist/cjs/generated/v3.d.ts +1149 -0
- package/dist/cjs/generated/v3.d.ts.map +1 -0
- package/dist/cjs/generated/v3.js +252 -0
- package/dist/cjs/generated/v3.js.map +1 -0
- package/dist/cjs/gwt-enrollments.d.ts +131 -0
- package/dist/cjs/gwt-enrollments.d.ts.map +1 -0
- package/dist/cjs/gwt-enrollments.js +280 -0
- package/dist/cjs/gwt-enrollments.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metadata.d.ts +100 -0
- package/dist/cjs/metadata.d.ts.map +1 -0
- package/dist/cjs/metadata.js +165 -0
- package/dist/cjs/metadata.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/program-discovery.d.ts +28 -0
- package/dist/cjs/program-discovery.d.ts.map +1 -0
- package/dist/cjs/program-discovery.js +58 -0
- package/dist/cjs/program-discovery.js.map +1 -0
- package/dist/cjs/race.d.ts +4 -0
- package/dist/cjs/race.d.ts.map +1 -0
- package/dist/cjs/race.js +40 -0
- package/dist/cjs/race.js.map +1 -0
- package/dist/cjs/runtime.d.ts +108 -0
- package/dist/cjs/runtime.d.ts.map +1 -0
- package/dist/cjs/runtime.js +38 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/subassessments.d.ts +29 -0
- package/dist/cjs/subassessments.d.ts.map +1 -0
- package/dist/cjs/subassessments.js +107 -0
- package/dist/cjs/subassessments.js.map +1 -0
- package/dist/cjs/testing.d.ts +45 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +66 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/transport.d.ts +19 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +373 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/v2.d.ts +13 -0
- package/dist/cjs/v2.d.ts.map +1 -0
- package/dist/cjs/v2.js +29 -0
- package/dist/cjs/v2.js.map +1 -0
- package/dist/cjs/v3.d.ts +13 -0
- package/dist/cjs/v3.d.ts.map +1 -0
- package/dist/cjs/v3.js +29 -0
- package/dist/cjs/v3.js.map +1 -0
- package/dist/cjs/validation.d.ts +33 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +77 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/verification.d.ts +82 -0
- package/dist/cjs/verification.d.ts.map +1 -0
- package/dist/cjs/verification.js +122 -0
- package/dist/cjs/verification.js.map +1 -0
- package/dist/esm/bulk.d.ts +33 -0
- package/dist/esm/bulk.d.ts.map +1 -0
- package/dist/esm/bulk.js +39 -0
- package/dist/esm/bulk.js.map +1 -0
- package/dist/esm/control.d.ts +35 -0
- package/dist/esm/control.d.ts.map +1 -0
- package/dist/esm/control.js +132 -0
- package/dist/esm/control.js.map +1 -0
- package/dist/esm/effective-date.d.ts +3 -0
- package/dist/esm/effective-date.d.ts.map +1 -0
- package/dist/esm/effective-date.js +24 -0
- package/dist/esm/effective-date.js.map +1 -0
- package/dist/esm/encoding.d.ts +32 -0
- package/dist/esm/encoding.d.ts.map +1 -0
- package/dist/esm/encoding.js +117 -0
- package/dist/esm/encoding.js.map +1 -0
- package/dist/esm/enrollment-verifications.d.ts +39 -0
- package/dist/esm/enrollment-verifications.d.ts.map +1 -0
- package/dist/esm/enrollment-verifications.js +93 -0
- package/dist/esm/enrollment-verifications.js.map +1 -0
- package/dist/esm/errors.d.ts +33 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/generated/v2-models.d.ts +1482 -0
- package/dist/esm/generated/v2-models.d.ts.map +1 -0
- package/dist/esm/generated/v2-models.js +2 -0
- package/dist/esm/generated/v2-models.js.map +1 -0
- package/dist/esm/generated/v2.d.ts +1149 -0
- package/dist/esm/generated/v2.d.ts.map +1 -0
- package/dist/esm/generated/v2.js +247 -0
- package/dist/esm/generated/v2.js.map +1 -0
- package/dist/esm/generated/v3-models.d.ts +1420 -0
- package/dist/esm/generated/v3-models.d.ts.map +1 -0
- package/dist/esm/generated/v3-models.js +2 -0
- package/dist/esm/generated/v3-models.js.map +1 -0
- package/dist/esm/generated/v3.d.ts +1149 -0
- package/dist/esm/generated/v3.d.ts.map +1 -0
- package/dist/esm/generated/v3.js +247 -0
- package/dist/esm/generated/v3.js.map +1 -0
- package/dist/esm/gwt-enrollments.d.ts +131 -0
- package/dist/esm/gwt-enrollments.d.ts.map +1 -0
- package/dist/esm/gwt-enrollments.js +274 -0
- package/dist/esm/gwt-enrollments.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/metadata.d.ts +100 -0
- package/dist/esm/metadata.d.ts.map +1 -0
- package/dist/esm/metadata.js +157 -0
- package/dist/esm/metadata.js.map +1 -0
- package/dist/esm/program-discovery.d.ts +28 -0
- package/dist/esm/program-discovery.d.ts.map +1 -0
- package/dist/esm/program-discovery.js +55 -0
- package/dist/esm/program-discovery.js.map +1 -0
- package/dist/esm/race.d.ts +4 -0
- package/dist/esm/race.d.ts.map +1 -0
- package/dist/esm/race.js +37 -0
- package/dist/esm/race.js.map +1 -0
- package/dist/esm/runtime.d.ts +108 -0
- package/dist/esm/runtime.d.ts.map +1 -0
- package/dist/esm/runtime.js +33 -0
- package/dist/esm/runtime.js.map +1 -0
- package/dist/esm/subassessments.d.ts +29 -0
- package/dist/esm/subassessments.d.ts.map +1 -0
- package/dist/esm/subassessments.js +103 -0
- package/dist/esm/subassessments.js.map +1 -0
- package/dist/esm/testing.d.ts +45 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +60 -0
- package/dist/esm/testing.js.map +1 -0
- package/dist/esm/transport.d.ts +19 -0
- package/dist/esm/transport.d.ts.map +1 -0
- package/dist/esm/transport.js +369 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/v2.d.ts +13 -0
- package/dist/esm/v2.d.ts.map +1 -0
- package/dist/esm/v2.js +13 -0
- package/dist/esm/v2.js.map +1 -0
- package/dist/esm/v3.d.ts +13 -0
- package/dist/esm/v3.d.ts.map +1 -0
- package/dist/esm/v3.js +13 -0
- package/dist/esm/v3.js.map +1 -0
- package/dist/esm/validation.d.ts +33 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +72 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/verification.d.ts +82 -0
- package/dist/esm/verification.d.ts.map +1 -0
- package/dist/esm/verification.js +117 -0
- package/dist/esm/verification.js.map +1 -0
- package/docs/README.md +56 -0
- package/docs/assessments.md +103 -0
- package/docs/browser-session.md +129 -0
- package/docs/capabilities.md +75 -0
- package/docs/configuration.md +81 -0
- package/docs/current-living-situation.md +56 -0
- package/docs/dates-and-answers.md +40 -0
- package/docs/development.md +81 -0
- package/docs/durable-records.md +156 -0
- package/docs/enrollments.md +217 -0
- package/docs/files-and-notes.md +39 -0
- package/docs/getting-started.md +56 -0
- package/docs/independent-needs.md +77 -0
- package/docs/integration-methods.md +60 -0
- package/docs/profiles.md +68 -0
- package/docs/reference.md +64 -0
- package/docs/rest-client.md +97 -0
- package/docs/services.md +66 -0
- package/docs/shelter-inventory.md +64 -0
- package/docs/shelters.md +75 -0
- package/docs/troubleshooting.md +61 -0
- package/docs/using-the-sdk.md +203 -0
- package/docs/vendor-issues/README.md +46 -0
- package/docs/vendor-issues/api-coverage.md +27 -0
- package/docs/vendor-issues/attachment-authentication.md +40 -0
- package/docs/vendor-issues/date-semantics.md +51 -0
- package/docs/vendor-issues/empty-service-list-404.md +51 -0
- package/docs/vendor-issues/enrollment-complete-target-error.md +178 -0
- package/docs/vendor-issues/enrollment-effective-date-column.md +112 -0
- package/docs/vendor-issues/household-head-validation.md +26 -0
- package/docs/vendor-issues/write-contracts.md +51 -0
- package/examples/README.md +41 -0
- package/examples/current-living-situation.mts +27 -0
- package/examples/durable-records.mts +78 -0
- package/examples/enrollment-workflow.mts +46 -0
- package/examples/gwt-enrollment-session.ts +17 -0
- package/examples/gwt-file-store.mjs +2 -0
- package/examples/household-enrollment-workflow.mts +43 -0
- package/examples/household-exit-workflow.mts +35 -0
- package/examples/household-lifecycle-workflow.mts +34 -0
- package/examples/independent-needs.mts +30 -0
- package/examples/program-discovery.ts +19 -0
- package/examples/quickstart.mjs +13 -0
- package/examples/shelter-inventory.mts +30 -0
- package/examples/shelters.mts +33 -0
- package/examples/usage.ts +23 -0
- package/examples/verification-snapshot.ts +9 -0
- package/llms.txt +26 -0
- package/package.json +103 -0
- package/specs/provenance.json +17 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { WellSkyError } from "./runtime.js";
|
|
2
|
+
export function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/** Validates the structural subset used by the pinned specifications. */
|
|
6
|
+
export function matches(value, schema, registry, mode, depth = 0) {
|
|
7
|
+
if (depth > 100)
|
|
8
|
+
return false;
|
|
9
|
+
if (value === null && schema.nullable)
|
|
10
|
+
return true;
|
|
11
|
+
if (schema.$ref) {
|
|
12
|
+
const target = registry[schema.$ref.split("/").at(-1) ?? ""];
|
|
13
|
+
return target !== undefined && matches(value, target, registry, mode, depth + 1);
|
|
14
|
+
}
|
|
15
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
16
|
+
return false;
|
|
17
|
+
switch (schema.type?.toLowerCase()) {
|
|
18
|
+
case "string":
|
|
19
|
+
if (schema.format === "binary")
|
|
20
|
+
return value instanceof Blob;
|
|
21
|
+
if (typeof value !== "string")
|
|
22
|
+
return false;
|
|
23
|
+
// Format strings and .NET regexes are not portable business validation.
|
|
24
|
+
return mode === "response" || ((schema.minLength === undefined || value.length >= schema.minLength) && (schema.maxLength === undefined || value.length <= schema.maxLength));
|
|
25
|
+
case "integer": return typeof value === "number" && Number.isSafeInteger(value);
|
|
26
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
27
|
+
case "boolean": return typeof value === "boolean";
|
|
28
|
+
case "array": {
|
|
29
|
+
const itemSchema = schema.items;
|
|
30
|
+
return Array.isArray(value) && itemSchema !== undefined && value.every(item => matches(item, itemSchema, registry, mode, depth + 1));
|
|
31
|
+
}
|
|
32
|
+
case "object": {
|
|
33
|
+
if (!isRecord(value))
|
|
34
|
+
return false;
|
|
35
|
+
if (schema.required?.some(key => !Object.hasOwn(value, key) || value[key] === undefined))
|
|
36
|
+
return false;
|
|
37
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
38
|
+
if (Object.hasOwn(value, key) && value[key] !== undefined && !matches(value[key], child, registry, mode, depth + 1))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Response fields added by WellSky are allowed. Request typos are rejected.
|
|
42
|
+
if (mode === "request" && schema.additionalProperties === false && Object.keys(value).some(key => !Object.hasOwn(schema.properties ?? {}, key)))
|
|
43
|
+
return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
default: return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function decodeResponse(value, schema, registry, operation) {
|
|
50
|
+
// No schema means the public return type is unknown; empty responses remain null.
|
|
51
|
+
if (!schema || value === null)
|
|
52
|
+
return value;
|
|
53
|
+
let valid;
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
valid = value.every(item => matches(item, schema, registry, "response"));
|
|
56
|
+
else if (isRecord(value) && Object.hasOwn(value, "value")) {
|
|
57
|
+
valid = Array.isArray(value.value) && value.value.every(item => matches(item, schema, registry, "response")) &&
|
|
58
|
+
(value["@odata.count"] === undefined || typeof value["@odata.count"] === "number" && Number.isSafeInteger(value["@odata.count"]) && value["@odata.count"] >= 0) &&
|
|
59
|
+
(value["@odata.nextLink"] === undefined || typeof value["@odata.nextLink"] === "string");
|
|
60
|
+
}
|
|
61
|
+
else if (isRecord(value) && (Object.hasOwn(value, "count") || Object.hasOwn(value, "values"))) {
|
|
62
|
+
valid = typeof value.count === "number" && Number.isSafeInteger(value.count) && value.count >= 0 &&
|
|
63
|
+
Array.isArray(value.values) && value.values.every(item => matches(item, schema, registry, "response"));
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
valid = matches(value, schema, registry, "response");
|
|
67
|
+
if (!valid)
|
|
68
|
+
throw new WellSkyError("protocol", operation);
|
|
69
|
+
// Generated return types and runtime schemas come from the same specification.
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { WellSkyError } from \"./runtime.js\";\n\nexport interface Schema {\n $ref?: string;\n type?: string;\n format?: string;\n nullable?: boolean;\n properties?: Record<string, Schema>;\n required?: string[];\n items?: Schema;\n additionalProperties?: boolean | Schema;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n default?: unknown;\n enum?: unknown[];\n}\n\nexport type SchemaRegistry = Record<string, Schema>;\nexport type Method = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\" | \"OPTIONS\";\nexport interface OperationDefinition {\n method: Method;\n path: string;\n authenticated: boolean;\n pathSchema: Schema;\n querySchema: Schema;\n bodySchema?: Schema;\n responseSchema?: Schema;\n multipart: boolean;\n pageable: boolean;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Validates the structural subset used by the pinned specifications. */\nexport function matches(value: unknown, schema: Schema, registry: SchemaRegistry, mode: \"request\" | \"response\", depth = 0): boolean {\n if (depth > 100) return false;\n if (value === null && schema.nullable) return true;\n if (schema.$ref) {\n const target = registry[schema.$ref.split(\"/\").at(-1) ?? \"\"];\n return target !== undefined && matches(value, target, registry, mode, depth + 1);\n }\n if (schema.enum && !schema.enum.includes(value)) return false;\n switch (schema.type?.toLowerCase()) {\n case \"string\":\n if (schema.format === \"binary\") return value instanceof Blob;\n if (typeof value !== \"string\") return false;\n // Format strings and .NET regexes are not portable business validation.\n return mode === \"response\" || ((schema.minLength === undefined || value.length >= schema.minLength) && (schema.maxLength === undefined || value.length <= schema.maxLength));\n case \"integer\": return typeof value === \"number\" && Number.isSafeInteger(value);\n case \"number\": return typeof value === \"number\" && Number.isFinite(value);\n case \"boolean\": return typeof value === \"boolean\";\n case \"array\": {\n const itemSchema = schema.items;\n return Array.isArray(value) && itemSchema !== undefined && value.every(item => matches(item, itemSchema, registry, mode, depth + 1));\n }\n case \"object\": {\n if (!isRecord(value)) return false;\n if (schema.required?.some(key => !Object.hasOwn(value, key) || value[key] === undefined)) return false;\n for (const [key, child] of Object.entries(schema.properties ?? {})) {\n if (Object.hasOwn(value, key) && value[key] !== undefined && !matches(value[key], child, registry, mode, depth + 1)) return false;\n }\n // Response fields added by WellSky are allowed. Request typos are rejected.\n if (mode === \"request\" && schema.additionalProperties === false && Object.keys(value).some(key => !Object.hasOwn(schema.properties ?? {}, key))) return false;\n return true;\n }\n default: return false;\n }\n}\n\nexport function decodeResponse<T>(value: unknown, schema: Schema | undefined, registry: SchemaRegistry, operation: string): T {\n // No schema means the public return type is unknown; empty responses remain null.\n if (!schema || value === null) return value as T;\n let valid: boolean;\n if (Array.isArray(value)) valid = value.every(item => matches(item, schema, registry, \"response\"));\n else if (isRecord(value) && Object.hasOwn(value, \"value\")) {\n valid = Array.isArray(value.value) && value.value.every(item => matches(item, schema, registry, \"response\")) &&\n (value[\"@odata.count\"] === undefined || typeof value[\"@odata.count\"] === \"number\" && Number.isSafeInteger(value[\"@odata.count\"]) && value[\"@odata.count\"] >= 0) &&\n (value[\"@odata.nextLink\"] === undefined || typeof value[\"@odata.nextLink\"] === \"string\");\n } else if (isRecord(value) && (Object.hasOwn(value, \"count\") || Object.hasOwn(value, \"values\"))) {\n valid = typeof value.count === \"number\" && Number.isSafeInteger(value.count) && value.count >= 0 &&\n Array.isArray(value.values) && value.values.every(item => matches(item, schema, registry, \"response\"));\n } else valid = matches(value, schema, registry, \"response\");\n if (!valid) throw new WellSkyError(\"protocol\", operation);\n // Generated return types and runtime schemas come from the same specification.\n return value as T;\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { WellSkyError } from "./runtime.js";
|
|
2
|
+
import type { RequestOptions } from "./runtime.js";
|
|
3
|
+
export type ReadOutcome<T> = {
|
|
4
|
+
kind: "found";
|
|
5
|
+
value: T;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "absent";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "unknown";
|
|
10
|
+
error: WellSkyError;
|
|
11
|
+
};
|
|
12
|
+
export interface ReadBackEvidence {
|
|
13
|
+
endpoint: string;
|
|
14
|
+
readAt: string;
|
|
15
|
+
ids: Record<string, number>;
|
|
16
|
+
fields: Record<string, "match" | "absent" | "different">;
|
|
17
|
+
}
|
|
18
|
+
export interface FieldMismatch {
|
|
19
|
+
field: string;
|
|
20
|
+
expected: string;
|
|
21
|
+
actual: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface AttemptInfo {
|
|
24
|
+
correlationId: string;
|
|
25
|
+
startedAt: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
httpStatus?: number;
|
|
28
|
+
}
|
|
29
|
+
interface AttemptResult {
|
|
30
|
+
requestHash: string;
|
|
31
|
+
attempt: AttemptInfo;
|
|
32
|
+
}
|
|
33
|
+
export type WriteResult<T> = (AttemptResult & {
|
|
34
|
+
status: "verified";
|
|
35
|
+
committed: true;
|
|
36
|
+
data: T;
|
|
37
|
+
evidence: ReadBackEvidence;
|
|
38
|
+
}) | (AttemptResult & {
|
|
39
|
+
status: "unverified";
|
|
40
|
+
committed: true | "unknown";
|
|
41
|
+
data?: Partial<T>;
|
|
42
|
+
mismatches?: FieldMismatch[];
|
|
43
|
+
evidence?: ReadBackEvidence;
|
|
44
|
+
error?: WellSkyError;
|
|
45
|
+
}) | (AttemptResult & {
|
|
46
|
+
status: "failed";
|
|
47
|
+
committed: false;
|
|
48
|
+
reason: "preflight" | "rejected" | "proven_absent";
|
|
49
|
+
error: WellSkyError;
|
|
50
|
+
});
|
|
51
|
+
/** Include tenant, target IDs, operation, effective date, and encoded answers in the input. */
|
|
52
|
+
export declare function semanticHash(value: unknown, options?: RequestOptions): Promise<string>;
|
|
53
|
+
/** Absence semantics are endpoint-specific. No HTTP status means absent by default. */
|
|
54
|
+
export declare function readOutcome<T>(reader: () => Promise<T>, options: {
|
|
55
|
+
operation: string;
|
|
56
|
+
isAbsent?: (value: T) => boolean;
|
|
57
|
+
absentStatuses?: readonly number[];
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}): Promise<ReadOutcome<T>>;
|
|
60
|
+
export interface ReadBackRecord<T> {
|
|
61
|
+
data: T;
|
|
62
|
+
ids: Record<string, number>;
|
|
63
|
+
fields: Record<string, unknown>;
|
|
64
|
+
/** Set only when the read proves this attempt committed, not just that an older record exists. */
|
|
65
|
+
commitConfirmed?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface VerificationPlan<T> extends RequestOptions {
|
|
68
|
+
operation: string;
|
|
69
|
+
readEndpoint: string;
|
|
70
|
+
semanticPayload: unknown;
|
|
71
|
+
expectedFields: Record<string, unknown>;
|
|
72
|
+
preflight?: (options: RequestOptions) => Promise<void>;
|
|
73
|
+
submit: (options: RequestOptions) => Promise<unknown>;
|
|
74
|
+
readBack: (options: RequestOptions) => Promise<ReadOutcome<ReadBackRecord<T>>>;
|
|
75
|
+
/** An authoritative proof supplied by an endpoint adapter, not merely an empty list or a 404. */
|
|
76
|
+
proveNotCommitted?: (options: RequestOptions) => Promise<boolean>;
|
|
77
|
+
mode?: "write" | "verify-only";
|
|
78
|
+
}
|
|
79
|
+
/** Sends at most once, always reads back, and never infers success or rollback from an HTTP status. */
|
|
80
|
+
export declare function verifyWrite<T>(plan: VerificationPlan<T>): Promise<WriteResult<T>>;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../src/verification.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { WellSkyError } from "./runtime.js";
|
|
2
|
+
import { AmbiguousCommitError, ValidationError } from "./errors.js";
|
|
3
|
+
function canonical(value, seen = new Set()) {
|
|
4
|
+
if (value === null)
|
|
5
|
+
return "null";
|
|
6
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
if (typeof value !== "object" || seen.has(value))
|
|
11
|
+
throw new ValidationError("semantic payload hash");
|
|
12
|
+
seen.add(value);
|
|
13
|
+
let result;
|
|
14
|
+
// Array.from visits holes as undefined, which canonical rejects. map skips them
|
|
15
|
+
// and would otherwise hash a one-hole array exactly like an empty array.
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
result = `[${Array.from(value, item => canonical(item, seen)).join(",")}]`;
|
|
18
|
+
else {
|
|
19
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
20
|
+
throw new ValidationError("semantic payload hash");
|
|
21
|
+
result = `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item, seen)}`).join(",")}}`;
|
|
22
|
+
}
|
|
23
|
+
seen.delete(value);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/** Include tenant, target IDs, operation, effective date, and encoded answers in the input. */
|
|
27
|
+
export async function semanticHash(value, options = {}) {
|
|
28
|
+
options.signal?.throwIfAborted();
|
|
29
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical(value)));
|
|
30
|
+
options.signal?.throwIfAborted();
|
|
31
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
32
|
+
}
|
|
33
|
+
function safeError(error, operation) {
|
|
34
|
+
return error instanceof WellSkyError ? error : new WellSkyError("protocol", operation);
|
|
35
|
+
}
|
|
36
|
+
/** Absence semantics are endpoint-specific. No HTTP status means absent by default. */
|
|
37
|
+
export async function readOutcome(reader, options) {
|
|
38
|
+
try {
|
|
39
|
+
options.signal?.throwIfAborted();
|
|
40
|
+
const value = await reader();
|
|
41
|
+
return options.isAbsent?.(value) ? { kind: "absent" } : { kind: "found", value };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const safe = safeError(error, options.operation);
|
|
45
|
+
if (safe.status !== undefined && options.absentStatuses?.includes(safe.status))
|
|
46
|
+
return { kind: "absent" };
|
|
47
|
+
return { kind: "unknown", error: safe };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Sends at most once, always reads back, and never infers success or rollback from an HTTP status. */
|
|
51
|
+
export async function verifyWrite(plan) {
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
const correlationId = plan.correlationId ?? crypto.randomUUID();
|
|
54
|
+
const context = { correlationId, ...(plan.signal ? { signal: plan.signal } : {}), ...(plan.timeoutMs === undefined ? {} : { timeoutMs: plan.timeoutMs }) };
|
|
55
|
+
// Hashing invalid semantic input throws before any network operation.
|
|
56
|
+
const requestHash = await semanticHash(plan.semanticPayload);
|
|
57
|
+
let httpStatus;
|
|
58
|
+
const base = () => ({ requestHash, attempt: { correlationId, startedAt: new Date(started).toISOString(), durationMs: Date.now() - started, ...(httpStatus === undefined ? {} : { httpStatus }) } });
|
|
59
|
+
try {
|
|
60
|
+
plan.signal?.throwIfAborted();
|
|
61
|
+
await plan.preflight?.(context);
|
|
62
|
+
if (!Object.keys(plan.expectedFields).length)
|
|
63
|
+
throw new ValidationError(plan.operation, ["expectedFields"]);
|
|
64
|
+
for (const value of Object.values(plan.expectedFields))
|
|
65
|
+
canonical(value);
|
|
66
|
+
plan.signal?.throwIfAborted();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { ...base(), status: "failed", committed: false, reason: "preflight", error: safeError(error, plan.operation) };
|
|
70
|
+
}
|
|
71
|
+
let writeError;
|
|
72
|
+
if (plan.mode !== "verify-only") {
|
|
73
|
+
try {
|
|
74
|
+
await plan.submit(context);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
writeError = safeError(error, plan.operation);
|
|
78
|
+
httpStatus = writeError.status;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let read;
|
|
82
|
+
try {
|
|
83
|
+
read = await plan.readBack(context);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
read = { kind: "unknown", error: safeError(error, plan.readEndpoint) };
|
|
87
|
+
}
|
|
88
|
+
if (read.kind === "unknown")
|
|
89
|
+
return { ...base(), status: "unverified", committed: "unknown", error: read.error };
|
|
90
|
+
if (read.kind === "absent") {
|
|
91
|
+
let proven = false;
|
|
92
|
+
try {
|
|
93
|
+
proven = await plan.proveNotCommitted?.(context) === true;
|
|
94
|
+
}
|
|
95
|
+
catch { /* An unavailable proof is not absence. */ }
|
|
96
|
+
const error = writeError ?? new AmbiguousCommitError(plan.operation);
|
|
97
|
+
return proven ? { ...base(), status: "failed", committed: false, reason: writeError?.status && writeError.status >= 400 && writeError.status < 500 ? "rejected" : "proven_absent", error } : { ...base(), status: "unverified", committed: "unknown", error };
|
|
98
|
+
}
|
|
99
|
+
const fields = {};
|
|
100
|
+
const mismatches = [];
|
|
101
|
+
for (const [field, expected] of Object.entries(plan.expectedFields)) {
|
|
102
|
+
const present = Object.hasOwn(read.value.fields, field);
|
|
103
|
+
let equal = false;
|
|
104
|
+
try {
|
|
105
|
+
equal = present && canonical(expected) === canonical(read.value.fields[field]);
|
|
106
|
+
}
|
|
107
|
+
catch { /* Malformed read values cannot verify a write. */ }
|
|
108
|
+
fields[field] = equal ? "match" : present ? "different" : "absent";
|
|
109
|
+
if (!equal)
|
|
110
|
+
mismatches.push({ field, expected: "[REDACTED]", actual: present ? "[REDACTED]" : null });
|
|
111
|
+
}
|
|
112
|
+
const evidence = { endpoint: plan.readEndpoint, readAt: new Date().toISOString(), ids: { ...read.value.ids }, fields };
|
|
113
|
+
if (!mismatches.length)
|
|
114
|
+
return { ...base(), status: "verified", committed: true, data: read.value.data, evidence };
|
|
115
|
+
return { ...base(), status: "unverified", committed: read.value.commitConfirmed === true ? true : "unknown", data: read.value.data, mismatches, evidence, ...(writeError ? { error: writeError } : {}) };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=verification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification.js","sourceRoot":"","sources":["../../src/verification.ts"],"names":[],"mappings":"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","sourcesContent":["import { WellSkyError } from \"./runtime.js\";\nimport type { RequestOptions } from \"./runtime.js\";\nimport { AmbiguousCommitError, ValidationError } from \"./errors.js\";\n\nexport type ReadOutcome<T> = { kind: \"found\"; value: T } | { kind: \"absent\" } | { kind: \"unknown\"; error: WellSkyError };\nexport interface ReadBackEvidence {\n endpoint: string;\n readAt: string;\n ids: Record<string, number>;\n fields: Record<string, \"match\" | \"absent\" | \"different\">;\n}\nexport interface FieldMismatch { field: string; expected: string; actual: string | null }\nexport interface AttemptInfo { correlationId: string; startedAt: string; durationMs: number; httpStatus?: number }\ninterface AttemptResult { requestHash: string; attempt: AttemptInfo }\nexport type WriteResult<T> =\n | (AttemptResult & { status: \"verified\"; committed: true; data: T; evidence: ReadBackEvidence })\n | (AttemptResult & { status: \"unverified\"; committed: true | \"unknown\"; data?: Partial<T>; mismatches?: FieldMismatch[]; evidence?: ReadBackEvidence; error?: WellSkyError })\n | (AttemptResult & { status: \"failed\"; committed: false; reason: \"preflight\" | \"rejected\" | \"proven_absent\"; error: WellSkyError });\n\nfunction canonical(value: unknown, seen = new Set<object>()): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\" || typeof value === \"boolean\") return JSON.stringify(value);\n if (typeof value === \"number\" && Number.isFinite(value)) return JSON.stringify(value);\n if (typeof value !== \"object\" || seen.has(value)) throw new ValidationError(\"semantic payload hash\");\n seen.add(value);\n let result: string;\n // Array.from visits holes as undefined, which canonical rejects. map skips them\n // and would otherwise hash a one-hole array exactly like an empty array.\n if (Array.isArray(value)) result = `[${Array.from(value, item => canonical(item, seen)).join(\",\")}]`;\n else {\n if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new ValidationError(\"semantic payload hash\");\n result = `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item, seen)}`).join(\",\")}}`;\n }\n seen.delete(value);\n return result;\n}\n\n/** Include tenant, target IDs, operation, effective date, and encoded answers in the input. */\nexport async function semanticHash(value: unknown, options: RequestOptions = {}): Promise<string> {\n options.signal?.throwIfAborted();\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(canonical(value)));\n options.signal?.throwIfAborted();\n return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction safeError(error: unknown, operation: string): WellSkyError {\n return error instanceof WellSkyError ? error : new WellSkyError(\"protocol\", operation);\n}\n\n/** Absence semantics are endpoint-specific. No HTTP status means absent by default. */\nexport async function readOutcome<T>(reader: () => Promise<T>, options: { operation: string; isAbsent?: (value: T) => boolean; absentStatuses?: readonly number[]; signal?: AbortSignal }): Promise<ReadOutcome<T>> {\n try {\n options.signal?.throwIfAborted();\n const value = await reader();\n return options.isAbsent?.(value) ? { kind: \"absent\" } : { kind: \"found\", value };\n } catch (error) {\n const safe = safeError(error, options.operation);\n if (safe.status !== undefined && options.absentStatuses?.includes(safe.status)) return { kind: \"absent\" };\n return { kind: \"unknown\", error: safe };\n }\n}\n\nexport interface ReadBackRecord<T> {\n data: T;\n ids: Record<string, number>;\n fields: Record<string, unknown>;\n /** Set only when the read proves this attempt committed, not just that an older record exists. */\n commitConfirmed?: boolean;\n}\nexport interface VerificationPlan<T> extends RequestOptions {\n operation: string;\n readEndpoint: string;\n semanticPayload: unknown;\n expectedFields: Record<string, unknown>;\n preflight?: (options: RequestOptions) => Promise<void>;\n submit: (options: RequestOptions) => Promise<unknown>;\n readBack: (options: RequestOptions) => Promise<ReadOutcome<ReadBackRecord<T>>>;\n /** An authoritative proof supplied by an endpoint adapter, not merely an empty list or a 404. */\n proveNotCommitted?: (options: RequestOptions) => Promise<boolean>;\n mode?: \"write\" | \"verify-only\";\n}\n\n/** Sends at most once, always reads back, and never infers success or rollback from an HTTP status. */\nexport async function verifyWrite<T>(plan: VerificationPlan<T>): Promise<WriteResult<T>> {\n const started = Date.now();\n const correlationId = plan.correlationId ?? crypto.randomUUID();\n const context: RequestOptions = { correlationId, ...(plan.signal ? { signal: plan.signal } : {}), ...(plan.timeoutMs === undefined ? {} : { timeoutMs: plan.timeoutMs }) };\n // Hashing invalid semantic input throws before any network operation.\n const requestHash = await semanticHash(plan.semanticPayload);\n let httpStatus: number | undefined;\n const base = (): AttemptResult => ({ requestHash, attempt: { correlationId, startedAt: new Date(started).toISOString(), durationMs: Date.now() - started, ...(httpStatus === undefined ? {} : { httpStatus }) } });\n try {\n plan.signal?.throwIfAborted();\n await plan.preflight?.(context);\n if (!Object.keys(plan.expectedFields).length) throw new ValidationError(plan.operation, [\"expectedFields\"]);\n for (const value of Object.values(plan.expectedFields)) canonical(value);\n plan.signal?.throwIfAborted();\n } catch (error) { return { ...base(), status: \"failed\", committed: false, reason: \"preflight\", error: safeError(error, plan.operation) }; }\n let writeError: WellSkyError | undefined;\n if (plan.mode !== \"verify-only\") {\n try { await plan.submit(context); }\n catch (error) { writeError = safeError(error, plan.operation); httpStatus = writeError.status; }\n }\n let read: ReadOutcome<ReadBackRecord<T>>;\n try { read = await plan.readBack(context); }\n catch (error) { read = { kind: \"unknown\", error: safeError(error, plan.readEndpoint) }; }\n if (read.kind === \"unknown\") return { ...base(), status: \"unverified\", committed: \"unknown\", error: read.error };\n if (read.kind === \"absent\") {\n let proven = false;\n try { proven = await plan.proveNotCommitted?.(context) === true; } catch { /* An unavailable proof is not absence. */ }\n const error = writeError ?? new AmbiguousCommitError(plan.operation);\n return proven ? { ...base(), status: \"failed\", committed: false, reason: writeError?.status && writeError.status >= 400 && writeError.status < 500 ? \"rejected\" : \"proven_absent\", error } : { ...base(), status: \"unverified\", committed: \"unknown\", error };\n }\n const fields: ReadBackEvidence[\"fields\"] = {};\n const mismatches: FieldMismatch[] = [];\n for (const [field, expected] of Object.entries(plan.expectedFields)) {\n const present = Object.hasOwn(read.value.fields, field);\n let equal = false;\n try { equal = present && canonical(expected) === canonical(read.value.fields[field]); } catch { /* Malformed read values cannot verify a write. */ }\n fields[field] = equal ? \"match\" : present ? \"different\" : \"absent\";\n if (!equal) mismatches.push({ field, expected: \"[REDACTED]\", actual: present ? \"[REDACTED]\" : null });\n }\n const evidence: ReadBackEvidence = { endpoint: plan.readEndpoint, readAt: new Date().toISOString(), ids: { ...read.value.ids }, fields };\n if (!mismatches.length) return { ...base(), status: \"verified\", committed: true, data: read.value.data, evidence };\n return { ...base(), status: \"unverified\", committed: read.value.commitConfirmed === true ? true : \"unknown\", data: read.value.data, mismatches, evidence, ...(writeError ? { error: writeError } : {}) };\n}\n"]}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Documentation index
|
|
2
|
+
|
|
3
|
+
[Package overview](../README.md) · [Agent instructions](../AGENTS.md)
|
|
4
|
+
|
|
5
|
+
These guides cover WellSky Community Services (HMIS), formerly ServicePoint. References to WellSky APIs here mean this platform's Client API, not APIs for other WellSky products.
|
|
6
|
+
|
|
7
|
+
## Learn the client
|
|
8
|
+
|
|
9
|
+
Start with [Using the SDK](using-the-sdk.md) for installation, authentication, REST and browser reads, workflow selection, and recovery. You can also run the [offline quickstart](getting-started.md). It runs a real SDK client against the built-in fake transport, with no HMIS account or writes.
|
|
10
|
+
|
|
11
|
+
Then read [REST configuration](rest-client.md) for authentication and tenant connection settings. Browser credentials have a separate contract in [browser sessions](browser-session.md).
|
|
12
|
+
|
|
13
|
+
## Understand the implementation choices
|
|
14
|
+
|
|
15
|
+
[Why the SDK uses REST, GWT-RPC, and browser forms](integration-methods.md) records the method used for each workflow, the limitation that led to it, alternatives rejected, and the recovery guarantees that must survive a future change.
|
|
16
|
+
|
|
17
|
+
## Complete a task
|
|
18
|
+
|
|
19
|
+
| Task | Guide | Example or contract |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| Create clients, manage households, and journal case plans | [Durable records](durable-records.md) | [Creation, membership, notes and recovery](../examples/durable-records.mts) |
|
|
22
|
+
| Read clients, page results, or run generic bulk calls | [REST client](rest-client.md) | [REST usage](../examples/usage.ts) |
|
|
23
|
+
| Inspect questions, resolve picklists, or encode answers | [Assessments](assessments.md) | [Program discovery](../examples/program-discovery.ts) |
|
|
24
|
+
| Read verification rows at enrollment | [Assessment snapshots](assessments.md#read-verification-records-at-enrollment) | [Verification snapshot](../examples/verification-snapshot.ts) |
|
|
25
|
+
| Update Client Record, Sex, or DOB | [Profiles](profiles.md) | [Helper signatures](reference.md#browser-helpers) |
|
|
26
|
+
| Create enrollment and interim assessments | [Enrollments](enrollments.md) | [Single client](../examples/enrollment-workflow.mts), [household](../examples/household-enrollment-workflow.mts) |
|
|
27
|
+
| Exit selected members or add a child later | [Household lifecycle](enrollments.md#later-household-members-and-staggered-exits) | [Lifecycle](../examples/household-lifecycle-workflow.mts), [exit](../examples/household-exit-workflow.mts) |
|
|
28
|
+
| Record services, SSVF details, or Funding Sources | [Services](services.md), [SSVF rules](../SSVF.md) | [Service contract](../browser/service-recording.d.mts) |
|
|
29
|
+
| Record a need without a service | [Independent needs](independent-needs.md) | [Need discovery and recording](../examples/independent-needs.mts) |
|
|
30
|
+
| Record outreach or shelter contacts | [Current Living Situation](current-living-situation.md) | Program-entry starts, contact dates, and distinct event identities |
|
|
31
|
+
| Understand shelter stays and enrollment coverage | [Shelters](shelters.md) | Household check-in/checkout, enrollment guards, and retry protection |
|
|
32
|
+
| Report shelter inventory and vacancies | [Shelter inventory](shelter-inventory.md) | Unit lists, named beds, occupants, holds, overflow, and bounded complete snapshots |
|
|
33
|
+
| Upload attachments or write goal case notes | [Files and notes](files-and-notes.md) | [Browser contracts](../browser/index.d.mts) |
|
|
34
|
+
| Supply a custom GWT session | [Session adapter](browser-session.md#experimental-gwt-enrollment-helpers) | [Adapter contract](../examples/gwt-enrollment-session.ts) |
|
|
35
|
+
| Recover an interrupted write | [Troubleshooting](troubleshooting.md) | Keep the original request and journal |
|
|
36
|
+
|
|
37
|
+
Most guide snippets are integration fragments with application-provided inputs. They do not run themselves. The offline quickstart is a complete executable example. Examples ship with the package. Package checks compile the TypeScript files and exercise the new workflows against a clean installed tarball with synthetic adapters; install `playwright-core` to compile examples that use its types.
|
|
38
|
+
|
|
39
|
+
## Look up a contract
|
|
40
|
+
|
|
41
|
+
- [Capabilities](capabilities.md) separates generated REST methods, tested helpers, and unsupported behavior.
|
|
42
|
+
- [Configuration reference](configuration.md) lists REST options and browser credentials.
|
|
43
|
+
- [Dates and answer semantics](dates-and-answers.md) distinguishes calendar DOBs, effective timestamps, and verification intervals.
|
|
44
|
+
- [Helper reference](reference.md) indexes all public runtime exports and declaration files.
|
|
45
|
+
- [Generated API table](../API.md) lists HTTP operations for both versions.
|
|
46
|
+
- [SSVF rules](../SSVF.md) lists conditional choices and required fields.
|
|
47
|
+
|
|
48
|
+
## Maintain the SDK
|
|
49
|
+
|
|
50
|
+
Follow [development and release](development.md) for checks, generated-code boundaries, and packaging. [Agent instructions](../AGENTS.md) provide the shorter operational entry point. [llms.txt](../llms.txt) provides a text-only documentation map.
|
|
51
|
+
|
|
52
|
+
Credentials and raw authenticated captures stay outside this package. The [capabilities guide](capabilities.md) retains sanitized acceptance evidence and limits; [WellSky feedback](vendor-issues/README.md) retains reproducible observations and support questions. These dated summaries do not claim that every provider or deployment has been tested.
|
|
53
|
+
|
|
54
|
+
## Report upstream issues
|
|
55
|
+
|
|
56
|
+
[WellSky feedback](vendor-issues/README.md) separates reproduced failures, contract questions, and missing public capabilities. The reports preserve evidence and SDK workarounds without claiming a confirmed root cause or a submitted support ticket.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Read and encode assessments
|
|
2
|
+
|
|
3
|
+
[Documentation index](README.md) · [Agent guide](../AGENTS.md)
|
|
4
|
+
|
|
5
|
+
## Assessments, questions, and picklists
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const context = {
|
|
9
|
+
clientId,
|
|
10
|
+
assessmentCode: assessmentCodeFromYourConfiguration,
|
|
11
|
+
effectiveDate: '2026-10-22',
|
|
12
|
+
};
|
|
13
|
+
const definition = await client.assessments.getDefinition(context);
|
|
14
|
+
const questions = await client.questions.list(context);
|
|
15
|
+
const question = await client.questions.get({ ...context, fieldName: fieldFromYourMapping });
|
|
16
|
+
const picklists = await client.picklists.list(context);
|
|
17
|
+
const option = await client.picklists.resolve({
|
|
18
|
+
...context,
|
|
19
|
+
fieldName: fieldFromYourMapping,
|
|
20
|
+
code: optionCodeFromYourConfiguration,
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
These are read-only operations. Definitions combine generic assessment reads with mobile metadata reads, preserving mandatory flags, nested questions, option IDs/codes/names, and unresolved lookups. Client answers and recordset data are excluded from definitions and their cache. Option catalogs can still contain private tenant labels; do not publish them.
|
|
25
|
+
|
|
26
|
+
Use `parentPath` to distinguish duplicate field names in different subassessments. Resolution prefers a supplied code, then a supplied name; missing or ambiguous options throw `UnknownPicklistValueError`. Picklists remain scoped to their questions so narrower allowed sets are not combined incorrectly.
|
|
27
|
+
|
|
28
|
+
The default metadata cache TTL is 60 seconds with 100 entries per client. Configure `metadataCacheTtlMs` and `metadataCacheMaxEntries`, pass `refresh: true`, or call `clearMetadataCache()`. Returned objects are copies. `compareDefinition(expected, definition)` compares expectations supplied by the application and reports added, removed, retyped, and newly mandatory fields.
|
|
29
|
+
|
|
30
|
+
WellSky requires a client context and known assessment code for these reads. There is no documented global assessment-list or standalone picklist-list endpoint. Mobile option metadata has no effective-date filter, so definitions explicitly identify its options as current even when the generic assessment read is dated. Metadata also does not provide a complete questionnaire branching engine.
|
|
31
|
+
|
|
32
|
+
Use the raw `assessments.get`, `assessmentsMobile.getAnswers`, and `enrollments.getAssessment` methods for recorded answers. Use `encodeAnswers` and `encodeNewSubassessmentRow` for endpoint-specific encoding. Mobile lookups use option IDs; standalone/enrollment lookups require an explicit `writeLabel` resolver with tenant-verified labels. Raw option names are not assumed to be valid write labels. Provider answers are refused for mobile writes because that endpoint can silently discard them. Answer-state code mappings and multi-value serialization are caller-supplied; a refusal is never converted to `No`.
|
|
33
|
+
|
|
34
|
+
## Race and ethnicity combinations
|
|
35
|
+
|
|
36
|
+
`encodeAnswers` recognizes the standard `HUD_RACEETHNICITY` field. It resolves single-category and multiple-category answers to the tenant's `RACE_ETH_COMBO_*` option IDs. A single White answer uses `RACE_ETH_COMBO_WHI`, even when the caller supplies `RACE_ETH_WHI`. Atomic option IDs can read back with a label while appearing blank in the UI.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const answers = encodeAnswers(definition.questions, {
|
|
40
|
+
HUD_RACEETHNICITY: {
|
|
41
|
+
options: [{ code: 'RACE_ETH_HSP' }, { code: 'RACE_ETH_PI' }],
|
|
42
|
+
},
|
|
43
|
+
}, { endpoint: 'mobile' });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Category order is normalized and duplicates are removed. Missing combinations and mixtures of categories with missing-answer states fail validation. Names resolve only among combination entries to avoid duplicate-label ambiguity. Other lookup fields keep their existing behavior. Raw methods still send the supplied answer payload; use the encoder rather than hardcoding numeric IDs. Standalone/enrollment encoding still requires `writeLabel` for the resolved combination option.
|
|
47
|
+
|
|
48
|
+
## Mobile effective-date correction
|
|
49
|
+
|
|
50
|
+
Live testing found that `POST /assessmentsmobile/AddAssessmentAnswer` recorded effective dates seven hours earlier than the requested instant, with `Z`, explicit offsets, and no suffix. Winter and summer test dates both shifted seven hours in that session. Standalone assessment writes preserved the requested instant. This does not establish the upstream timezone or guarantee that its offset stays constant.
|
|
51
|
+
|
|
52
|
+
For a deployment with a measured mobile shift, configure `mobileAssessmentDateCorrectionMinutes` on `WellSkyClient`. For example, **420 adds seven hours to the outgoing mobile query**, compensating for a verified upstream subtraction of seven hours. The default is unset and preserves raw behavior; the SDK never infers a correction from the host timezone or tenant name.
|
|
53
|
+
|
|
54
|
+
With correction enabled, supply an ISO timestamp with `Z` or an explicit numeric offset. Invalid calendar dates, ambiguous timestamps, and correction values outside integer minutes from -840 to 840 are rejected before authentication/network activity. Only the mobile write's `dateEffective` query is corrected. Read queries, standalone/enrollment writes, and date values inside answer bodies are unchanged. Do not pre-adjust dates as well as configuring the client.
|
|
55
|
+
|
|
56
|
+
Verify `date_effective` through a generic assessment read and check UI history after writes. Revalidate the correction when upstream behavior changes, including seasonal clock changes; it is a measured compatibility setting, not automatic timezone handling. Mobile reads can omit UI-visible combination answers, so an empty mobile answer alone does not prove absence. Standard standalone writes are preferable for scalar answers when that endpoint fits the application; mobile remains necessary for verified in-place recordset updates.
|
|
57
|
+
|
|
58
|
+
## Read verification records at enrollment
|
|
59
|
+
|
|
60
|
+
`readEnrollmentVerifications` explicitly reads the current generic assessment and filters the four standard groups: income, non-cash benefits, health insurance, and disabilities. This avoids the observed complete-enrollment entry projection defect, which excluded records near enrollment even when they were created correctly through the UI.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { readEnrollmentVerifications } from '@choiceapps/communityservices-sdk';
|
|
64
|
+
|
|
65
|
+
const snapshot = await readEnrollmentVerifications(client, {
|
|
66
|
+
clientId,
|
|
67
|
+
assessmentCode, // Your instrument must expose all four verification groups.
|
|
68
|
+
enrollmentDate: '2026-10-18T12:00:00-04:00',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const income = snapshot.groups.find(g => g.fieldName === 'MONTHLYINCOME');
|
|
72
|
+
// income.records contains the original recordset IDs and answer fields.
|
|
73
|
+
// income.excluded explains each exclusion as "not_started" or "ended".
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Available from the root, V2, and V3 entry points in ESM and CommonJS. The helper makes one assessment GET, without `dateEffective`, and forwards `signal`, `timeoutMs`, and `correlationId`. An effective-date read can omit backdated records, so selection uses each row's own start/end values instead of its answer effective timestamp or `isActive` flag.
|
|
77
|
+
|
|
78
|
+
Intervals are inclusive: **start <= enrollment <= end**, with null/empty ends open. Comparisons use exact instants, including fractional seconds; end dates are not rounded to the end of a calendar day. Supply an enrollment timestamp with `Z` or a numeric offset. Offset-bearing ISO and WellSky SQL-style row dates such as `2026-10-18 09:00:00-07` are supported. No host timezone, seven-hour correction, or date-only timezone is inferred. Invalid input fails before a read. Missing/duplicate groups or date fields, invalid/ambiguous intervals, and duplicate record IDs produce protocol errors instead of silently implying zero records. Present empty groups return empty arrays.
|
|
79
|
+
|
|
80
|
+
Results contain detached copies of the original records and dates, plus `source: 'current-assessment'`. The helper never writes, changes stored dates, verifies HUD badges, merges overlapping records, or changes the raw complete-enrollment response. It returns the currently accessible record versions filtered by dates, not an audit-history reconstruction: later edits and access restrictions can affect what the assessment API exposes. No tenant-specific assessment code or expected type count is built in.
|
|
81
|
+
|
|
82
|
+
## End-date an existing subassessment
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const result = await client.subassessments.setEndDate({
|
|
86
|
+
clientId,
|
|
87
|
+
assessmentCode,
|
|
88
|
+
parentField,
|
|
89
|
+
recordsetId: 456,
|
|
90
|
+
endDateField,
|
|
91
|
+
endDate: '2026-10-22',
|
|
92
|
+
effectiveDate: '2026-10-22T12:00:00-04:00',
|
|
93
|
+
preflight: async (options) => { /* Apply your program's review-date and eligibility rules. */ },
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is generic recordset handling. It reads the current row, targets its existing `recordset_id` with `isDeleted: false`, preserves the other returned values, and verifies the end date, preserved values, and recordset list. It never deletes/recreates the row or retries the write. The result is a `WriteResult`, including `unverified` if the server silently ignores the update. Rows containing provider questions are refused because mobile writes cannot preserve those values reliably.
|
|
98
|
+
|
|
99
|
+
An October 22, 2026 sandbox probe confirmed that an existing recordset could be end-dated in place and retained the same ID. Application policy decides when historical records should end or be replaced; the SDK verifies the requested operation.
|
|
100
|
+
|
|
101
|
+
## Current Living Situation history
|
|
102
|
+
|
|
103
|
+
The [contact recorder](current-living-situation.md) retains one row per meaningful contact, with program-entry start, contact information date, actual living-situation answer, and an open end. The complete-enrollment scalar is not the contact history. Optional Verified By may identify the recording program using its discovered question representation.
|