@happyvertical/smrt-fields 0.40.60 → 0.40.61
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 +58 -12
- package/README.md +90 -2
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +77 -0
- package/dist/field-definitions.d.ts.map +1 -0
- package/dist/field-policy-resolver.d.ts +46 -0
- package/dist/field-policy-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +9 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +752 -55
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +45 -3
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +39 -0
- package/dist/playground.js.map +1 -0
- package/dist/settings-catalog.d.ts +73 -0
- package/dist/settings-catalog.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +50 -7
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +223 -0
- package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
- package/dist/svelte/__tests__/FormHelp.test.js +138 -0
- package/dist/svelte/__tests__/ModeUX.test.js +109 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +556 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +20 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
- package/dist/svelte/components/AdvancedFields.svelte +49 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldInput.svelte +159 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +261 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +26 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte +17 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +135 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +17 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FormHelp.svelte +157 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ModeSwitch.svelte +47 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectForm.svelte +351 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +40 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PolicyField.svelte +338 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.d.ts +54 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.js +55 -0
- package/dist/svelte/data-table.d.ts +17 -0
- package/dist/svelte/data-table.d.ts.map +1 -0
- package/dist/svelte/data-table.js +19 -0
- package/dist/svelte/field-policy-editor.d.ts +81 -0
- package/dist/svelte/field-policy-editor.d.ts.map +1 -0
- package/dist/svelte/field-policy-editor.js +120 -0
- package/dist/svelte/gear-context.svelte.d.ts +14 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
- package/dist/svelte/gear-context.svelte.js +15 -0
- package/dist/svelte/index.d.ts +58 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +38 -0
- package/dist/svelte/input-registry.d.ts +21 -0
- package/dist/svelte/input-registry.d.ts.map +1 -0
- package/dist/svelte/input-registry.js +26 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source-context.svelte.js +8 -0
- package/dist/svelte/object-form-source.svelte.d.ts +18 -0
- package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source.svelte.js +99 -0
- package/dist/svelte/object-form.d.ts +6 -0
- package/dist/svelte/object-form.d.ts.map +1 -0
- package/dist/svelte/object-form.js +38 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +20 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +38 -0
- package/dist/svelte/settings-catalog.d.ts +54 -0
- package/dist/svelte/settings-catalog.d.ts.map +1 -0
- package/dist/svelte/settings-catalog.js +116 -0
- package/dist/svelte/types.d.ts +101 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +4 -0
- package/dist/types.d.ts +276 -203
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +33 -8
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
// @vitest-environment jsdom
|
|
8
|
+
/**
|
|
9
|
+
* Browser form -> generated web definition -> generated REST integration.
|
|
10
|
+
* Definitions are built from the Vitest plugin's real manifest through core's
|
|
11
|
+
* shared web-definition builder; this test intentionally has no field maps.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { TextDecoder, TextEncoder } from 'node:util';
|
|
16
|
+
import { vi } from 'vitest';
|
|
17
|
+
vi.hoisted(() => {
|
|
18
|
+
// The jsdom transform serves package shims through an http URL; this
|
|
19
|
+
// integration owns explicit local registrations, so avoid an unrelated
|
|
20
|
+
// package-manifest diagnostic while exercising the generated handler.
|
|
21
|
+
process.env.SMRT_STRICT_REGISTRY = 'false';
|
|
22
|
+
});
|
|
23
|
+
// sql/esbuild behind the generated handler requires TextEncoder and Uint8Array
|
|
24
|
+
// from one realm. jsdom replaces the globals, so restore Node's encoder before
|
|
25
|
+
// the in-process server begins handling requests.
|
|
26
|
+
globalThis.TextEncoder = TextEncoder;
|
|
27
|
+
globalThis.TextDecoder = TextDecoder;
|
|
28
|
+
globalThis.Uint8Array = new TextEncoder().encode('')
|
|
29
|
+
.constructor;
|
|
30
|
+
import { APIGenerator, field, ObjectRegistry, SmrtCollection, SmrtObject, smrt, } from '@happyvertical/smrt-core';
|
|
31
|
+
import { getTestDatabase } from '@happyvertical/smrt-core/testing';
|
|
32
|
+
import { resetTenancy, setupTestTenancy, withTenant, } from '@happyvertical/smrt-tenancy';
|
|
33
|
+
import { render, screen, userEvent, within, } from '@happyvertical/smrt-vitest/svelte';
|
|
34
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
35
|
+
import { buildWebCollectionDefinition, selectWebCollectionEntries, } from '../../../../core/src/vite-plugin/web-collections.js';
|
|
36
|
+
import { FieldPolicyCollection } from '../../collections/FieldPolicyCollection.js';
|
|
37
|
+
import { MANAGE_FIELD_POLICY_PERMISSION, PERSONALIZE_FIELD_POLICY_PERMISSION, } from '../../permissions.js';
|
|
38
|
+
import { ObjectFormSourceRegistry } from '../index.js';
|
|
39
|
+
import GeneratedApiGearFixture from './fixtures/GeneratedApiGearFixture.svelte';
|
|
40
|
+
import ObjectFormSourceFixture from './fixtures/ObjectFormSourceFixture.svelte';
|
|
41
|
+
import ObjectFormSourceSnippetFixture from './fixtures/ObjectFormSourceSnippetFixture.svelte';
|
|
42
|
+
let GeneratedApiProduct = class GeneratedApiProduct extends SmrtObject {
|
|
43
|
+
name = '';
|
|
44
|
+
price = 0;
|
|
45
|
+
published = false;
|
|
46
|
+
};
|
|
47
|
+
__decorate([
|
|
48
|
+
field({ required: true })
|
|
49
|
+
], GeneratedApiProduct.prototype, "name", void 0);
|
|
50
|
+
__decorate([
|
|
51
|
+
field({ type: 'decimal' })
|
|
52
|
+
], GeneratedApiProduct.prototype, "price", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
field({ type: 'boolean', default: false })
|
|
55
|
+
], GeneratedApiProduct.prototype, "published", void 0);
|
|
56
|
+
GeneratedApiProduct = __decorate([
|
|
57
|
+
smrt({
|
|
58
|
+
packageName: '@happyvertical/smrt-fields',
|
|
59
|
+
visibility: 'internal',
|
|
60
|
+
api: { include: ['list', 'get', 'create', 'update'] },
|
|
61
|
+
cli: false,
|
|
62
|
+
mcp: false,
|
|
63
|
+
})
|
|
64
|
+
], GeneratedApiProduct);
|
|
65
|
+
let GeneratedApiEvent = class GeneratedApiEvent extends SmrtObject {
|
|
66
|
+
title = '';
|
|
67
|
+
startsAt = '';
|
|
68
|
+
metadata = {};
|
|
69
|
+
};
|
|
70
|
+
__decorate([
|
|
71
|
+
field({ required: true })
|
|
72
|
+
], GeneratedApiEvent.prototype, "title", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
field({ type: 'datetime' })
|
|
75
|
+
], GeneratedApiEvent.prototype, "startsAt", void 0);
|
|
76
|
+
__decorate([
|
|
77
|
+
field({ type: 'json' })
|
|
78
|
+
], GeneratedApiEvent.prototype, "metadata", void 0);
|
|
79
|
+
GeneratedApiEvent = __decorate([
|
|
80
|
+
smrt({
|
|
81
|
+
packageName: '@happyvertical/smrt-fields',
|
|
82
|
+
visibility: 'internal',
|
|
83
|
+
api: { include: ['list', 'get', 'create', 'update'] },
|
|
84
|
+
cli: false,
|
|
85
|
+
mcp: false,
|
|
86
|
+
})
|
|
87
|
+
], GeneratedApiEvent);
|
|
88
|
+
class GeneratedApiProductCollection extends SmrtCollection {
|
|
89
|
+
static _itemClass = GeneratedApiProduct;
|
|
90
|
+
}
|
|
91
|
+
class GeneratedApiEventCollection extends SmrtCollection {
|
|
92
|
+
static _itemClass = GeneratedApiEvent;
|
|
93
|
+
}
|
|
94
|
+
const passThroughAuth = () => async (request) => request;
|
|
95
|
+
function ref(ctor) {
|
|
96
|
+
const registered = ObjectRegistry.getClassByConstructor(ctor);
|
|
97
|
+
if (!registered?.qualifiedName)
|
|
98
|
+
throw new Error('generated API fixture was not registered');
|
|
99
|
+
return registered.qualifiedName;
|
|
100
|
+
}
|
|
101
|
+
function generatedDefinitions() {
|
|
102
|
+
const manifest = JSON.parse(readFileSync(resolve(process.cwd(), 'dist/manifest.json'), 'utf8'));
|
|
103
|
+
return Object.fromEntries(selectWebCollectionEntries(manifest).map((entry) => [
|
|
104
|
+
entry.obj.qualifiedName,
|
|
105
|
+
buildWebCollectionDefinition(entry, manifest),
|
|
106
|
+
]));
|
|
107
|
+
}
|
|
108
|
+
describe('ObjectForm generated API integration', () => {
|
|
109
|
+
let db;
|
|
110
|
+
let policies;
|
|
111
|
+
let handler;
|
|
112
|
+
let definitions;
|
|
113
|
+
const tenantId = '8cd50df6-7cfb-4fcf-b06f-9d8acb55f22e';
|
|
114
|
+
const aliceId = '61514216-1d0b-4a6e-b501-20f5d8baf86e';
|
|
115
|
+
const bobId = '24657fd2-2813-4c43-b5af-0c916cbce3bc';
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
setupTestTenancy();
|
|
118
|
+
db = await getTestDatabase({
|
|
119
|
+
classes: [
|
|
120
|
+
'FieldPolicy',
|
|
121
|
+
'Tenant',
|
|
122
|
+
'GeneratedApiProduct',
|
|
123
|
+
'GeneratedApiEvent',
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
policies = await FieldPolicyCollection.create({ db });
|
|
127
|
+
const products = await GeneratedApiProductCollection.create({ db });
|
|
128
|
+
const events = await GeneratedApiEventCollection.create({ db });
|
|
129
|
+
const api = new APIGenerator({ authMiddleware: passThroughAuth }, { db });
|
|
130
|
+
api.registerCollection('fieldpolicy', policies);
|
|
131
|
+
api.registerCollection('generatedapiproduct', products);
|
|
132
|
+
api.registerCollection('generatedapievent', events);
|
|
133
|
+
handler = api.generateHandler();
|
|
134
|
+
definitions = generatedDefinitions();
|
|
135
|
+
});
|
|
136
|
+
afterEach(async () => {
|
|
137
|
+
resetTenancy();
|
|
138
|
+
await db?.close?.();
|
|
139
|
+
});
|
|
140
|
+
async function requestAs(userId, permissions, path, init) {
|
|
141
|
+
return withTenant({ tenantId, userId, permissions }, () => handler(new Request(`http://localhost/api/v1${path}`, init)));
|
|
142
|
+
}
|
|
143
|
+
function request(path, init) {
|
|
144
|
+
return requestAs(aliceId, new Set(), path, init);
|
|
145
|
+
}
|
|
146
|
+
function sourceFor(objectRef, userId = aliceId, permissions = new Set()) {
|
|
147
|
+
const definition = definitions[objectRef];
|
|
148
|
+
if (!definition)
|
|
149
|
+
throw new Error(`missing generated definition for ${objectRef}`);
|
|
150
|
+
const resolveBatch = vi.fn(async (input) => {
|
|
151
|
+
const response = await requestAs(userId, permissions, '/fieldpolicy/resolve', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'content-type': 'application/json' },
|
|
154
|
+
body: JSON.stringify(input),
|
|
155
|
+
});
|
|
156
|
+
if (response.status !== 200) {
|
|
157
|
+
throw new Error(`resolveBatch HTTP ${response.status}: ${await response.text()}`);
|
|
158
|
+
}
|
|
159
|
+
const envelope = (await response.json());
|
|
160
|
+
expect(envelope.action).toBe('resolveBatch');
|
|
161
|
+
return envelope.result;
|
|
162
|
+
});
|
|
163
|
+
const source = new ObjectFormSourceRegistry({ resolveBatch });
|
|
164
|
+
source.register(definition);
|
|
165
|
+
return source;
|
|
166
|
+
}
|
|
167
|
+
function gearAdapter(userId, permissions) {
|
|
168
|
+
async function action(path, init) {
|
|
169
|
+
const response = await requestAs(userId, permissions, path, init);
|
|
170
|
+
const body = await response.json().catch(() => undefined);
|
|
171
|
+
return { status: response.status, body };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
async load(input) {
|
|
175
|
+
const result = await action('/fieldpolicy/editor-state', {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'content-type': 'application/json' },
|
|
178
|
+
body: JSON.stringify(input),
|
|
179
|
+
});
|
|
180
|
+
if (result.status === 403)
|
|
181
|
+
return result.body.error;
|
|
182
|
+
if (result.status !== 200)
|
|
183
|
+
throw new Error(`editor-state HTTP ${result.status}`);
|
|
184
|
+
const envelope = result.body;
|
|
185
|
+
expect(envelope.action).toBe('getEditorState');
|
|
186
|
+
return envelope.result;
|
|
187
|
+
},
|
|
188
|
+
async create(input) {
|
|
189
|
+
const result = await action('/fieldpolicy', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'content-type': 'application/json' },
|
|
192
|
+
body: JSON.stringify(input),
|
|
193
|
+
});
|
|
194
|
+
if (result.status !== 201)
|
|
195
|
+
throw new Error(`create HTTP ${result.status}`);
|
|
196
|
+
return result.body;
|
|
197
|
+
},
|
|
198
|
+
async update(input) {
|
|
199
|
+
const { id, ...body } = input;
|
|
200
|
+
const result = await action(`/fieldpolicy/${id}`, {
|
|
201
|
+
method: 'PUT',
|
|
202
|
+
headers: { 'content-type': 'application/json' },
|
|
203
|
+
body: JSON.stringify(body),
|
|
204
|
+
});
|
|
205
|
+
if (result.status !== 200)
|
|
206
|
+
throw new Error(`update HTTP ${result.status}`);
|
|
207
|
+
return result.body;
|
|
208
|
+
},
|
|
209
|
+
async delete(input) {
|
|
210
|
+
const response = await requestAs(userId, permissions, `/fieldpolicy/${input.id}`, { method: 'DELETE' });
|
|
211
|
+
if (response.status !== 204)
|
|
212
|
+
throw new Error(`delete HTTP ${response.status}`);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
it('renders two generated definitions with no per-object fields and persists create/update via generated routes', async () => {
|
|
217
|
+
const productRef = ref(GeneratedApiProduct);
|
|
218
|
+
const eventRef = ref(GeneratedApiEvent);
|
|
219
|
+
const productDefinition = definitions[productRef];
|
|
220
|
+
const eventDefinition = definitions[eventRef];
|
|
221
|
+
await expect(withTenant({ tenantId, userId: aliceId, permissions: new Set() }, () => policies.resolveBatch({ objectRefs: [productRef] }))).resolves.toMatchObject({
|
|
222
|
+
policies: { [productRef]: { objectRef: productRef } },
|
|
223
|
+
});
|
|
224
|
+
expect(productDefinition.fields).toMatchObject({
|
|
225
|
+
name: { type: 'text' },
|
|
226
|
+
price: { type: 'decimal' },
|
|
227
|
+
published: { type: 'boolean' },
|
|
228
|
+
});
|
|
229
|
+
expect(eventDefinition.fields).toMatchObject({
|
|
230
|
+
title: { type: 'text' },
|
|
231
|
+
startsAt: { type: 'datetime' },
|
|
232
|
+
metadata: { type: 'json' },
|
|
233
|
+
});
|
|
234
|
+
const productView = render(ObjectFormSourceSnippetFixture, {
|
|
235
|
+
props: { source: sourceFor(productRef), objectRef: productRef },
|
|
236
|
+
});
|
|
237
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Custom generated name' })).toBeInTheDocument());
|
|
238
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Custom generated name' }), 'launch');
|
|
239
|
+
await userEvent.type(screen.getByRole('spinbutton'), '19.5');
|
|
240
|
+
const productRecord = JSON.parse(screen.getByTestId('record').textContent ?? '{}');
|
|
241
|
+
expect(productRecord).toMatchObject({ name: 'LAUNCH', price: 19.5 });
|
|
242
|
+
const created = await request(productDefinition.endpoint, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'content-type': 'application/json' },
|
|
245
|
+
body: JSON.stringify(productRecord),
|
|
246
|
+
});
|
|
247
|
+
expect(created.status).toBe(201);
|
|
248
|
+
const product = (await created.json());
|
|
249
|
+
expect(product).toMatchObject({ name: 'LAUNCH', price: 19.5 });
|
|
250
|
+
const updated = await request(`${productDefinition.endpoint}/${product.id}`, {
|
|
251
|
+
method: 'PUT',
|
|
252
|
+
headers: { 'content-type': 'application/json' },
|
|
253
|
+
body: JSON.stringify({ price: 22 }),
|
|
254
|
+
});
|
|
255
|
+
expect(updated.status).toBe(200);
|
|
256
|
+
expect(await updated.json()).toMatchObject({ id: product.id, price: 22 });
|
|
257
|
+
productView.unmount();
|
|
258
|
+
render(ObjectFormSourceFixture, {
|
|
259
|
+
props: {
|
|
260
|
+
source: sourceFor(eventRef),
|
|
261
|
+
objectRef: eventRef,
|
|
262
|
+
isNewRecord: false,
|
|
263
|
+
initialValue: {
|
|
264
|
+
title: 'Existing event',
|
|
265
|
+
startsAt: '2026-08-08T10:00',
|
|
266
|
+
metadata: { venue: 'Hall A' },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Title' })).toHaveValue('Existing event'));
|
|
271
|
+
expect(screen.getByRole('textbox', { name: 'Metadata' })
|
|
272
|
+
.value).toContain('Hall A');
|
|
273
|
+
await userEvent.clear(screen.getByRole('textbox', { name: 'Title' }));
|
|
274
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Title' }), 'Updated event');
|
|
275
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Updated event');
|
|
276
|
+
const eventRecord = JSON.parse(screen.getByTestId('record').textContent ?? '{}');
|
|
277
|
+
const createdEvent = await request(eventDefinition.endpoint, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: { 'content-type': 'application/json' },
|
|
280
|
+
body: JSON.stringify(eventRecord),
|
|
281
|
+
});
|
|
282
|
+
expect(createdEvent.status).toBe(201);
|
|
283
|
+
const event = (await createdEvent.json());
|
|
284
|
+
expect(event).toMatchObject({
|
|
285
|
+
title: 'Updated event',
|
|
286
|
+
metadata: { venue: 'Hall A' },
|
|
287
|
+
});
|
|
288
|
+
const updatedEvent = await request(`${eventDefinition.endpoint}/${event.id}`, {
|
|
289
|
+
method: 'PUT',
|
|
290
|
+
headers: { 'content-type': 'application/json' },
|
|
291
|
+
body: JSON.stringify({ title: 'Republished event' }),
|
|
292
|
+
});
|
|
293
|
+
expect(updatedEvent.status).toBe(200);
|
|
294
|
+
expect(await updatedEvent.json()).toMatchObject({
|
|
295
|
+
id: event.id,
|
|
296
|
+
title: 'Republished event',
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
it('drives gear organization, personal, lock, reset, and denial through APIGenerator', async () => {
|
|
300
|
+
const eventRef = ref(GeneratedApiEvent);
|
|
301
|
+
const fields = definitions[eventRef].fields;
|
|
302
|
+
const alicePermissions = new Set([
|
|
303
|
+
MANAGE_FIELD_POLICY_PERMISSION,
|
|
304
|
+
PERSONALIZE_FIELD_POLICY_PERMISSION,
|
|
305
|
+
]);
|
|
306
|
+
const bobPermissions = new Set([PERSONALIZE_FIELD_POLICY_PERMISSION]);
|
|
307
|
+
const alice = render(GeneratedApiGearFixture, {
|
|
308
|
+
props: {
|
|
309
|
+
objectRef: eventRef,
|
|
310
|
+
fields,
|
|
311
|
+
adapter: gearAdapter(aliceId, alicePermissions),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Field settings' })).toBeInTheDocument());
|
|
315
|
+
await userEvent.click(screen.getByRole('button', { name: 'Field settings' }));
|
|
316
|
+
await vi.waitFor(() => expect(screen.getByRole('dialog', { name: 'Field settings' })).toBeInTheDocument());
|
|
317
|
+
const organizationTitle = screen.getByRole('group', { name: 'title' });
|
|
318
|
+
await userEvent.type(within(organizationTitle).getByRole('textbox', { name: 'Label' }), 'Organization title');
|
|
319
|
+
await userEvent.click(within(organizationTitle).getByRole('button', { name: 'Save' }));
|
|
320
|
+
await vi.waitFor(() => expect(screen.getByRole('dialog', { name: 'Field settings' })).toBeInTheDocument());
|
|
321
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Just me' }));
|
|
322
|
+
const personalTitle = screen.getByRole('group', {
|
|
323
|
+
name: 'Organization title',
|
|
324
|
+
});
|
|
325
|
+
await userEvent.clear(within(personalTitle).getByRole('textbox', { name: 'Label' }));
|
|
326
|
+
await userEvent.type(within(personalTitle).getByRole('textbox', { name: 'Label' }), 'Alice title');
|
|
327
|
+
await userEvent.click(within(personalTitle).getByRole('button', { name: 'Save' }));
|
|
328
|
+
await vi.waitFor(async () => {
|
|
329
|
+
const state = (await gearAdapter(aliceId, alicePermissions).load({
|
|
330
|
+
objectRef: eventRef,
|
|
331
|
+
}));
|
|
332
|
+
expect(state.policy.fields.title.label).toBe('Alice title');
|
|
333
|
+
});
|
|
334
|
+
alice.unmount();
|
|
335
|
+
await expect(gearAdapter(bobId, bobPermissions).load({ objectRef: eventRef })).resolves.toMatchObject({
|
|
336
|
+
policy: { fields: { title: { label: 'Organization title' } } },
|
|
337
|
+
});
|
|
338
|
+
const bobForm = render(ObjectFormSourceFixture, {
|
|
339
|
+
props: {
|
|
340
|
+
source: sourceFor(eventRef, bobId, bobPermissions),
|
|
341
|
+
objectRef: eventRef,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Organization title' })).toBeInTheDocument());
|
|
345
|
+
bobForm.unmount();
|
|
346
|
+
const aliceReset = render(GeneratedApiGearFixture, {
|
|
347
|
+
props: {
|
|
348
|
+
objectRef: eventRef,
|
|
349
|
+
fields,
|
|
350
|
+
adapter: gearAdapter(aliceId, alicePermissions),
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Field settings' })).toBeInTheDocument());
|
|
354
|
+
await userEvent.click(screen.getByRole('button', { name: 'Field settings' }));
|
|
355
|
+
await userEvent.click(await screen.findByRole('tab', { name: 'Just me' }));
|
|
356
|
+
const resetTitle = await screen.findByRole('group', {
|
|
357
|
+
name: 'Alice title',
|
|
358
|
+
});
|
|
359
|
+
await userEvent.click(within(resetTitle).getByRole('button', { name: 'Reset to inherited' }));
|
|
360
|
+
await vi.waitFor(async () => {
|
|
361
|
+
const state = (await gearAdapter(aliceId, alicePermissions).load({
|
|
362
|
+
objectRef: eventRef,
|
|
363
|
+
}));
|
|
364
|
+
expect(state.policy.fields.title.label).toBe('Organization title');
|
|
365
|
+
});
|
|
366
|
+
aliceReset.unmount();
|
|
367
|
+
const aliceLock = render(GeneratedApiGearFixture, {
|
|
368
|
+
props: {
|
|
369
|
+
objectRef: eventRef,
|
|
370
|
+
fields,
|
|
371
|
+
adapter: gearAdapter(aliceId, alicePermissions),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Field settings' })).toBeInTheDocument());
|
|
375
|
+
await userEvent.click(screen.getByRole('button', { name: 'Field settings' }));
|
|
376
|
+
const lockTitle = await screen.findByRole('group', {
|
|
377
|
+
name: 'Organization title',
|
|
378
|
+
});
|
|
379
|
+
await userEvent.click(within(lockTitle).getByRole('checkbox', {
|
|
380
|
+
name: 'Lock Organization title',
|
|
381
|
+
}));
|
|
382
|
+
await userEvent.click(within(lockTitle).getByRole('button', { name: 'Save' }));
|
|
383
|
+
aliceLock.unmount();
|
|
384
|
+
const bob = render(GeneratedApiGearFixture, {
|
|
385
|
+
props: {
|
|
386
|
+
objectRef: eventRef,
|
|
387
|
+
fields,
|
|
388
|
+
adapter: gearAdapter(bobId, bobPermissions),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Field settings' })).toBeInTheDocument());
|
|
392
|
+
await userEvent.click(screen.getByRole('button', { name: 'Field settings' }));
|
|
393
|
+
await userEvent.click(await screen.findByRole('tab', { name: 'Just me' }));
|
|
394
|
+
const lockedTitle = await screen.findByRole('group', {
|
|
395
|
+
name: 'Organization title',
|
|
396
|
+
});
|
|
397
|
+
expect(within(lockedTitle).getByText(/locked by your organization/i)).toBeInTheDocument();
|
|
398
|
+
expect(within(lockedTitle).getByRole('button', { name: 'Save' })).toBeDisabled();
|
|
399
|
+
const lockedWrite = await requestAs(bobId, bobPermissions, '/fieldpolicy', {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: { 'content-type': 'application/json' },
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
objectRef: eventRef,
|
|
404
|
+
fieldName: 'title',
|
|
405
|
+
scopeType: 'user',
|
|
406
|
+
defaultValue: null,
|
|
407
|
+
displayOrder: null,
|
|
408
|
+
help: null,
|
|
409
|
+
label: 'Rejected',
|
|
410
|
+
locked: null,
|
|
411
|
+
visibility: null,
|
|
412
|
+
}),
|
|
413
|
+
});
|
|
414
|
+
expect(lockedWrite.status).toBe(403);
|
|
415
|
+
expect(await lockedWrite.json()).toMatchObject({
|
|
416
|
+
error: {
|
|
417
|
+
code: 'permission_denied',
|
|
418
|
+
message: expect.stringMatching(/locked by org policy/i),
|
|
419
|
+
ok: false,
|
|
420
|
+
status: 403,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
bob.unmount();
|
|
424
|
+
const denied = render(GeneratedApiGearFixture, {
|
|
425
|
+
props: {
|
|
426
|
+
objectRef: eventRef,
|
|
427
|
+
fields,
|
|
428
|
+
adapter: gearAdapter('4adca9ba-13e9-473a-bb6c-b0a4c9972c1d', new Set()),
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
await vi.waitFor(() => expect(screen.queryByText('Loading field settings…')).not.toBeInTheDocument());
|
|
432
|
+
expect(screen.queryByRole('button', { name: 'Field settings' })).not.toBeInTheDocument();
|
|
433
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
434
|
+
denied.unmount();
|
|
435
|
+
});
|
|
436
|
+
});
|