@object-ui/data-objectstack 2.0.0 → 3.0.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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Contracts module integration for @objectstack/spec v3.0.0
11
+ * Provides plugin contract validation and marketplace publishing utilities.
12
+ */
13
+
14
+ export interface PluginContract {
15
+ /** Plugin name */
16
+ name: string;
17
+ /** Plugin version */
18
+ version: string;
19
+ /** Required peer dependencies */
20
+ peerDependencies?: Record<string, string>;
21
+ /** Exported component types */
22
+ exports: PluginExport[];
23
+ /** Required permissions */
24
+ permissions?: string[];
25
+ /** API surface contract */
26
+ api?: PluginAPIContract;
27
+ }
28
+
29
+ export interface PluginExport {
30
+ /** Export name */
31
+ name: string;
32
+ /** Export type */
33
+ type: 'component' | 'hook' | 'utility' | 'provider';
34
+ /** Description */
35
+ description?: string;
36
+ }
37
+
38
+ export interface PluginAPIContract {
39
+ /** Consumed data sources */
40
+ dataSources?: string[];
41
+ /** Required object schemas */
42
+ requiredSchemas?: string[];
43
+ /** Event subscriptions */
44
+ events?: string[];
45
+ }
46
+
47
+ export interface ContractValidationResult {
48
+ valid: boolean;
49
+ errors: ContractValidationError[];
50
+ warnings: string[];
51
+ }
52
+
53
+ export interface ContractValidationError {
54
+ field: string;
55
+ message: string;
56
+ code: string;
57
+ }
58
+
59
+ /**
60
+ * Validate a plugin contract against the ObjectStack spec.
61
+ */
62
+ export function validatePluginContract(contract: PluginContract): ContractValidationResult {
63
+ const errors: ContractValidationError[] = [];
64
+ const warnings: string[] = [];
65
+
66
+ if (!contract.name || contract.name.trim().length === 0) {
67
+ errors.push({ field: 'name', message: 'Plugin name is required', code: 'MISSING_NAME' });
68
+ }
69
+
70
+ if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
71
+ errors.push({ field: 'version', message: 'Valid semver version is required', code: 'INVALID_VERSION' });
72
+ }
73
+
74
+ if (!contract.exports || contract.exports.length === 0) {
75
+ errors.push({ field: 'exports', message: 'At least one export is required', code: 'NO_EXPORTS' });
76
+ }
77
+
78
+ if (contract.exports) {
79
+ const validTypes = ['component', 'hook', 'utility', 'provider'];
80
+ for (const exp of contract.exports) {
81
+ if (!exp.name) {
82
+ errors.push({ field: 'exports.name', message: 'Export name is required', code: 'MISSING_EXPORT_NAME' });
83
+ }
84
+ if (!validTypes.includes(exp.type)) {
85
+ errors.push({ field: 'exports.type', message: `Invalid export type: ${exp.type}`, code: 'INVALID_EXPORT_TYPE' });
86
+ }
87
+ }
88
+ }
89
+
90
+ if (!contract.permissions || contract.permissions.length === 0) {
91
+ warnings.push('No permissions declared — plugin will have minimal access');
92
+ }
93
+
94
+ return { valid: errors.length === 0, errors, warnings };
95
+ }
96
+
97
+ /**
98
+ * Generate a plugin contract manifest for marketplace publishing.
99
+ */
100
+ export function generateContractManifest(contract: PluginContract): Record<string, unknown> {
101
+ return {
102
+ $schema: 'https://objectui.org/schemas/plugin-contract-v1.json',
103
+ name: contract.name,
104
+ version: contract.version,
105
+ peerDependencies: contract.peerDependencies ?? {},
106
+ exports: contract.exports.map(exp => ({
107
+ name: exp.name,
108
+ type: exp.type,
109
+ description: exp.description ?? '',
110
+ })),
111
+ permissions: contract.permissions ?? [],
112
+ api: contract.api ?? {},
113
+ generatedAt: new Date().toISOString(),
114
+ };
115
+ }
package/src/index.ts CHANGED
@@ -268,14 +268,16 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
268
268
  };
269
269
  }
270
270
 
271
- const resultObj = result as { value?: T[]; count?: number };
271
+ const resultObj = result as { records?: T[]; total?: number; value?: T[]; count?: number };
272
+ const records = resultObj.records || resultObj.value || [];
273
+ const total = resultObj.total ?? resultObj.count ?? records.length;
272
274
  return {
273
- data: resultObj.value || [],
274
- total: resultObj.count || (resultObj.value ? resultObj.value.length : 0),
275
+ data: records,
276
+ total,
275
277
  // Calculate page number safely
276
278
  page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
277
279
  pageSize: params?.$top,
278
- hasMore: params?.$top ? (resultObj.value?.length || 0) === params.$top : false,
280
+ hasMore: params?.$top ? records.length === params.$top : false,
279
281
  };
280
282
  }
281
283
 
@@ -557,7 +559,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
557
559
  try {
558
560
  // Use cache with automatic fetching
559
561
  const schema = await this.metadataCache.get(objectName, async () => {
560
- const result: any = await this.client.meta.getObject(objectName);
562
+ const result: any = await this.client.meta.getItem('object', objectName);
561
563
 
562
564
  // Unwrap 'item' property if present (common API response wrapper)
563
565
  if (result && result.item) {
@@ -667,6 +669,56 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
667
669
  }
668
670
  }
669
671
 
672
+ /**
673
+ * Get a page definition from ObjectStack.
674
+ * Uses the metadata API to fetch page layouts.
675
+ * Returns null if the server doesn't support page metadata.
676
+ */
677
+ async getPage(pageId: string): Promise<unknown | null> {
678
+ await this.connect();
679
+
680
+ try {
681
+ const cacheKey = `page:${pageId}`;
682
+ return await this.metadataCache.get(cacheKey, async () => {
683
+ const result: any = await this.client.meta.getItem('pages', pageId);
684
+ if (result && result.item) return result.item;
685
+ return result ?? null;
686
+ });
687
+ } catch {
688
+ // Server doesn't support page metadata — return null to fall back to static config
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Get multiple metadata items from ObjectStack.
695
+ * Uses v3.0.0 metadata API pattern: getItems for batch retrieval.
696
+ */
697
+ async getItems(category: string, names: string[]): Promise<unknown[]> {
698
+ await this.connect();
699
+
700
+ const results = await Promise.all(
701
+ names.map(async (name) => {
702
+ const cacheKey = `${category}:${name}`;
703
+ return this.metadataCache.get(cacheKey, async () => {
704
+ const result: any = await this.client.meta.getItem(category, name);
705
+ if (result && result.item) return result.item;
706
+ return result;
707
+ });
708
+ })
709
+ );
710
+
711
+ return results;
712
+ }
713
+
714
+ /**
715
+ * Get cached metadata if available, without triggering a fetch.
716
+ * Uses v3.0.0 metadata API pattern: getCached for synchronous cache access.
717
+ */
718
+ getCached(key: string): unknown | undefined {
719
+ return this.metadataCache.getCachedSync(key);
720
+ }
721
+
670
722
  /**
671
723
  * Get cache statistics for monitoring performance.
672
724
  */
@@ -860,3 +912,19 @@ export {
860
912
 
861
913
  // Export cache types
862
914
  export type { CacheStats } from './cache/MetadataCache';
915
+
916
+ // v3.0.0 Deep Integration modules
917
+ export { CloudOperations } from './cloud';
918
+ export type { CloudDeploymentConfig, CloudHostingConfig, CloudMarketplaceEntry } from './cloud';
919
+
920
+ export { validatePluginContract, generateContractManifest } from './contracts';
921
+ export type { PluginContract, PluginExport, PluginAPIContract, ContractValidationResult, ContractValidationError } from './contracts';
922
+
923
+ export { IntegrationManager } from './integration';
924
+ export type { IntegrationConfig, IntegrationTrigger, IntegrationProvider, SlackIntegrationConfig, EmailIntegrationConfig, WebhookIntegrationConfig } from './integration';
925
+
926
+ export { SecurityManager } from './security';
927
+ export type { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
928
+
929
+ export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
930
+ export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Integration module for @objectstack/spec v3.0.0
11
+ * Provides third-party service connectors for Slack, email, webhooks.
12
+ */
13
+
14
+ export type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
15
+
16
+ export interface IntegrationConfig {
17
+ /** Integration provider type */
18
+ provider: IntegrationProvider;
19
+ /** Whether this integration is enabled */
20
+ enabled: boolean;
21
+ /** Provider-specific configuration */
22
+ config: Record<string, unknown>;
23
+ /** Event triggers */
24
+ triggers?: IntegrationTrigger[];
25
+ }
26
+
27
+ export interface IntegrationTrigger {
28
+ /** Event name (e.g. 'record.created', 'record.updated') */
29
+ event: string;
30
+ /** Filter condition */
31
+ filter?: string;
32
+ /** Template for the message/payload */
33
+ template?: string;
34
+ }
35
+
36
+ export interface SlackIntegrationConfig extends IntegrationConfig {
37
+ provider: 'slack';
38
+ config: {
39
+ webhookUrl: string;
40
+ channel?: string;
41
+ username?: string;
42
+ iconEmoji?: string;
43
+ };
44
+ }
45
+
46
+ export interface EmailIntegrationConfig extends IntegrationConfig {
47
+ provider: 'email';
48
+ config: {
49
+ smtpHost: string;
50
+ smtpPort: number;
51
+ secure: boolean;
52
+ from: string;
53
+ to: string[];
54
+ subject?: string;
55
+ };
56
+ }
57
+
58
+ export interface WebhookIntegrationConfig extends IntegrationConfig {
59
+ provider: 'webhook';
60
+ config: {
61
+ url: string;
62
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH';
63
+ headers?: Record<string, string>;
64
+ retryCount?: number;
65
+ retryDelay?: number;
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Integration service manager.
71
+ * Manages third-party service connections and event dispatching.
72
+ */
73
+ export class IntegrationManager {
74
+ private integrations: Map<string, IntegrationConfig> = new Map();
75
+
76
+ /**
77
+ * Register a new integration.
78
+ */
79
+ register(id: string, config: IntegrationConfig): void {
80
+ this.integrations.set(id, config);
81
+ }
82
+
83
+ /**
84
+ * Remove an integration.
85
+ */
86
+ unregister(id: string): void {
87
+ this.integrations.delete(id);
88
+ }
89
+
90
+ /**
91
+ * Get all registered integrations.
92
+ */
93
+ getAll(): Map<string, IntegrationConfig> {
94
+ return new Map(this.integrations);
95
+ }
96
+
97
+ /**
98
+ * Get integrations that match a specific event.
99
+ */
100
+ getForEvent(event: string): IntegrationConfig[] {
101
+ return Array.from(this.integrations.values()).filter(
102
+ (integration) =>
103
+ integration.enabled &&
104
+ integration.triggers?.some((t) => t.event === event)
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Dispatch an event to all matching integrations.
110
+ * Returns results for each integration.
111
+ */
112
+ async dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{ id: string; success: boolean; error?: string }>> {
113
+ const matching = this.getForEvent(event);
114
+ const results: Array<{ id: string; success: boolean; error?: string }> = [];
115
+
116
+ for (const [id, integration] of this.integrations) {
117
+ if (!matching.includes(integration)) continue;
118
+
119
+ try {
120
+ await this.send(integration, payload);
121
+ results.push({ id, success: true });
122
+ } catch (err) {
123
+ results.push({ id, success: false, error: (err as Error).message });
124
+ }
125
+ }
126
+
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * Send payload to a specific integration.
132
+ */
133
+ private async send(integration: IntegrationConfig, payload: Record<string, unknown>): Promise<void> {
134
+ switch (integration.provider) {
135
+ case 'webhook': {
136
+ const cfg = integration.config as WebhookIntegrationConfig['config'];
137
+ const url = cfg.url;
138
+ // Validate URL - only allow http and https protocols
139
+ try {
140
+ const parsed = new URL(url);
141
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
142
+ throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
143
+ }
144
+ } catch (e) {
145
+ if (e instanceof TypeError) {
146
+ throw new Error(`Invalid webhook URL: ${url}`);
147
+ }
148
+ throw e;
149
+ }
150
+ await fetch(url, {
151
+ method: cfg.method,
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ ...cfg.headers,
155
+ },
156
+ body: JSON.stringify(payload),
157
+ });
158
+ break;
159
+ }
160
+ case 'slack': {
161
+ const cfg = integration.config as SlackIntegrationConfig['config'];
162
+ const url = cfg.webhookUrl;
163
+ // Validate URL - only allow https protocol for Slack webhooks
164
+ try {
165
+ const parsed = new URL(url);
166
+ if (parsed.protocol !== 'https:') {
167
+ throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
168
+ }
169
+ } catch (e) {
170
+ if (e instanceof TypeError) {
171
+ throw new Error(`Invalid Slack webhook URL: ${url}`);
172
+ }
173
+ throw e;
174
+ }
175
+ await fetch(url, {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({
179
+ channel: cfg.channel,
180
+ username: cfg.username,
181
+ icon_emoji: cfg.iconEmoji,
182
+ text: JSON.stringify(payload),
183
+ }),
184
+ });
185
+ break;
186
+ }
187
+ // Email and other providers would require server-side implementation
188
+ default:
189
+ break;
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Security module integration for @objectstack/spec v3.0.0
11
+ * Provides advanced security policies: CSP config, audit logging, data masking.
12
+ */
13
+
14
+ export interface SecurityPolicy {
15
+ /** Content Security Policy configuration */
16
+ csp?: CSPConfig;
17
+ /** Audit logging configuration */
18
+ auditLog?: AuditLogConfig;
19
+ /** Data masking rules */
20
+ dataMasking?: DataMaskingConfig;
21
+ }
22
+
23
+ export interface CSPConfig {
24
+ /** Script sources */
25
+ scriptSrc: string[];
26
+ /** Style sources */
27
+ styleSrc: string[];
28
+ /** Image sources */
29
+ imgSrc: string[];
30
+ /** Connect sources (APIs, WebSockets) */
31
+ connectSrc: string[];
32
+ /** Font sources */
33
+ fontSrc: string[];
34
+ /** Frame sources */
35
+ frameSrc?: string[];
36
+ /** Report URI for violations */
37
+ reportUri?: string;
38
+ }
39
+
40
+ export interface AuditLogConfig {
41
+ /** Whether audit logging is enabled */
42
+ enabled: boolean;
43
+ /** Events to audit */
44
+ events: AuditEventType[];
45
+ /** Log retention days */
46
+ retentionDays?: number;
47
+ /** External log destination */
48
+ destination?: 'console' | 'server' | 'both';
49
+ }
50
+
51
+ export type AuditEventType =
52
+ | 'auth.login'
53
+ | 'auth.logout'
54
+ | 'auth.failed'
55
+ | 'data.create'
56
+ | 'data.read'
57
+ | 'data.update'
58
+ | 'data.delete'
59
+ | 'admin.config'
60
+ | 'admin.user'
61
+ | 'admin.role';
62
+
63
+ export interface DataMaskingConfig {
64
+ /** Fields to mask */
65
+ rules: DataMaskingRule[];
66
+ /** Default masking character */
67
+ maskChar?: string;
68
+ }
69
+
70
+ export interface DataMaskingRule {
71
+ /** Field name or pattern */
72
+ field: string;
73
+ /** Masking strategy */
74
+ strategy: 'full' | 'partial' | 'hash' | 'redact';
75
+ /** Number of visible characters (for partial masking) */
76
+ visibleChars?: number;
77
+ /** Roles that can see unmasked data */
78
+ exemptRoles?: string[];
79
+ }
80
+
81
+ export interface AuditLogEntry {
82
+ /** Event timestamp */
83
+ timestamp: string;
84
+ /** Event type */
85
+ event: AuditEventType;
86
+ /** User who triggered the event */
87
+ userId: string;
88
+ /** Target resource */
89
+ resource?: string;
90
+ /** Record ID */
91
+ recordId?: string;
92
+ /** Additional details */
93
+ details?: Record<string, unknown>;
94
+ /** IP address */
95
+ ipAddress?: string;
96
+ }
97
+
98
+ /**
99
+ * Security policy manager.
100
+ * Handles CSP generation, audit logging, and data masking.
101
+ */
102
+ export class SecurityManager {
103
+ private policy: SecurityPolicy;
104
+ private auditLog: AuditLogEntry[] = [];
105
+
106
+ constructor(policy: SecurityPolicy = {}) {
107
+ this.policy = policy;
108
+ }
109
+
110
+ /**
111
+ * Generate a CSP header string from the configuration.
112
+ */
113
+ generateCSPHeader(): string {
114
+ const csp = this.policy.csp;
115
+ if (!csp) return '';
116
+
117
+ const directives: string[] = [];
118
+
119
+ if (csp.scriptSrc?.length) directives.push(`script-src ${csp.scriptSrc.join(' ')}`);
120
+ if (csp.styleSrc?.length) directives.push(`style-src ${csp.styleSrc.join(' ')}`);
121
+ if (csp.imgSrc?.length) directives.push(`img-src ${csp.imgSrc.join(' ')}`);
122
+ if (csp.connectSrc?.length) directives.push(`connect-src ${csp.connectSrc.join(' ')}`);
123
+ if (csp.fontSrc?.length) directives.push(`font-src ${csp.fontSrc.join(' ')}`);
124
+ if (csp.frameSrc?.length) directives.push(`frame-src ${csp.frameSrc.join(' ')}`);
125
+ if (csp.reportUri) directives.push(`report-uri ${csp.reportUri}`);
126
+
127
+ return directives.join('; ');
128
+ }
129
+
130
+ /**
131
+ * Record an audit log entry.
132
+ */
133
+ recordAudit(entry: Omit<AuditLogEntry, 'timestamp'>): void {
134
+ if (!this.policy.auditLog?.enabled) return;
135
+ if (this.policy.auditLog.events && !this.policy.auditLog.events.includes(entry.event)) return;
136
+
137
+ const fullEntry: AuditLogEntry = {
138
+ ...entry,
139
+ timestamp: new Date().toISOString(),
140
+ };
141
+
142
+ this.auditLog.push(fullEntry);
143
+
144
+ const dest = this.policy.auditLog.destination ?? 'console';
145
+ if (dest === 'console' || dest === 'both') {
146
+ console.info('[AUDIT]', fullEntry.event, fullEntry.userId, fullEntry.resource ?? '');
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get audit log entries.
152
+ */
153
+ getAuditLog(filter?: { event?: AuditEventType; userId?: string; since?: string }): AuditLogEntry[] {
154
+ let entries = [...this.auditLog];
155
+ if (filter?.event) entries = entries.filter(e => e.event === filter.event);
156
+ if (filter?.userId) entries = entries.filter(e => e.userId === filter.userId);
157
+ if (filter?.since) entries = entries.filter(e => e.timestamp >= filter.since!);
158
+ return entries;
159
+ }
160
+
161
+ /**
162
+ * Apply data masking to a record.
163
+ */
164
+ maskRecord(record: Record<string, unknown>, userRoles: string[] = []): Record<string, unknown> {
165
+ if (!this.policy.dataMasking?.rules?.length) return record;
166
+
167
+ const masked = { ...record };
168
+ const maskChar = this.policy.dataMasking.maskChar ?? '*';
169
+
170
+ for (const rule of this.policy.dataMasking.rules) {
171
+ if (!(rule.field in masked) || masked[rule.field] == null) continue;
172
+
173
+ // Check role exemptions
174
+ if (rule.exemptRoles?.some(role => userRoles.includes(role))) continue;
175
+
176
+ const value = String(masked[rule.field]);
177
+
178
+ switch (rule.strategy) {
179
+ case 'full':
180
+ masked[rule.field] = maskChar.repeat(value.length);
181
+ break;
182
+ case 'partial': {
183
+ const visible = rule.visibleChars ?? 4;
184
+ // Values shorter than visibleChars are fully masked for security
185
+ if (value.length <= visible) {
186
+ masked[rule.field] = maskChar.repeat(value.length);
187
+ } else {
188
+ masked[rule.field] = value.slice(0, visible) + maskChar.repeat(value.length - visible);
189
+ }
190
+ break;
191
+ }
192
+ case 'hash':
193
+ masked[rule.field] = `[HASHED:${simpleHash(value)}]`;
194
+ break;
195
+ case 'redact':
196
+ masked[rule.field] = '[REDACTED]';
197
+ break;
198
+ }
199
+ }
200
+
201
+ return masked;
202
+ }
203
+
204
+ /**
205
+ * Update the security policy.
206
+ */
207
+ updatePolicy(policy: Partial<SecurityPolicy>): void {
208
+ this.policy = { ...this.policy, ...policy };
209
+ }
210
+
211
+ /**
212
+ * Get current security policy.
213
+ */
214
+ getPolicy(): SecurityPolicy {
215
+ return { ...this.policy };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Simple hash function for data masking (not cryptographic).
221
+ */
222
+ function simpleHash(str: string): string {
223
+ let hash = 0;
224
+ for (let i = 0; i < str.length; i++) {
225
+ const char = str.charCodeAt(i);
226
+ hash = ((hash << 5) - hash) + char;
227
+ hash = hash & hash; // Convert to 32-bit integer
228
+ }
229
+ return Math.abs(hash).toString(36);
230
+ }