@yunsoft/yuncms-core 0.1.0 → 0.1.2

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,146 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { compileFieldColumn } from './field-types.js';
4
+ import { quoteIdentifier } from './identifier.js';
5
+
6
+ export const COLLECTION_SYSTEM_FIELDS = Object.freeze([
7
+ 'created_at',
8
+ 'updated_at',
9
+ 'created_by',
10
+ 'updated_by',
11
+ ]);
12
+
13
+ const SYSTEM_FIELD_DEFINITIONS = Object.freeze({
14
+ created_at: Object.freeze({
15
+ field: 'created_at',
16
+ type: 'timestamp',
17
+ required: true,
18
+ readonly: true,
19
+ interface: 'datetime',
20
+ special: 'date-created',
21
+ defaultPreset: 'now',
22
+ }),
23
+ updated_at: Object.freeze({
24
+ field: 'updated_at',
25
+ type: 'timestamp',
26
+ required: true,
27
+ readonly: true,
28
+ interface: 'datetime',
29
+ special: 'date-updated',
30
+ defaultPreset: 'now',
31
+ autoUpdate: true,
32
+ }),
33
+ created_by: Object.freeze({
34
+ field: 'created_by',
35
+ type: 'uuid',
36
+ required: false,
37
+ readonly: true,
38
+ interface: 'user',
39
+ special: 'user-created',
40
+ }),
41
+ updated_by: Object.freeze({
42
+ field: 'updated_by',
43
+ type: 'uuid',
44
+ required: false,
45
+ readonly: true,
46
+ interface: 'user',
47
+ special: 'user-updated',
48
+ }),
49
+ });
50
+
51
+ function invalidSystemFields(message) {
52
+ const error = new Error(message);
53
+ error.code = 'INVALID_SCHEMA_PAYLOAD';
54
+ return error;
55
+ }
56
+
57
+ export function normalizeCollectionSystemFields(value) {
58
+ if (value == null) return [];
59
+ if (!Array.isArray(value)) throw invalidSystemFields('systemFields must be an array');
60
+
61
+ const normalized = [...new Set(value)];
62
+ for (const field of normalized) {
63
+ if (!COLLECTION_SYSTEM_FIELDS.includes(field)) {
64
+ throw invalidSystemFields(`Unsupported system field: ${String(field)}`);
65
+ }
66
+ }
67
+ return COLLECTION_SYSTEM_FIELDS.filter((field) => normalized.includes(field));
68
+ }
69
+
70
+ export function systemFieldDefinition(field) {
71
+ return SYSTEM_FIELD_DEFINITIONS[field] ?? null;
72
+ }
73
+
74
+ function foreignKeyName(collection, field) {
75
+ const digest = createHash('sha256')
76
+ .update(`${collection}:${field}:system-user`)
77
+ .digest('hex')
78
+ .slice(0, 24);
79
+ return `ysf_${digest}`;
80
+ }
81
+
82
+ export function compileCollectionSystemFields(collection, requestedFields) {
83
+ const fields = normalizeCollectionSystemFields(requestedFields);
84
+ const columns = [];
85
+ const constraints = [];
86
+ const metadata = [];
87
+
88
+ for (const field of fields) {
89
+ const definition = SYSTEM_FIELD_DEFINITIONS[field];
90
+ const compiled = compileFieldColumn(definition);
91
+ columns.push(`${quoteIdentifier(field, 'system field')} ${compiled.sql}`);
92
+ metadata.push({
93
+ collection,
94
+ field,
95
+ type: definition.type,
96
+ required: definition.required,
97
+ readonly: true,
98
+ hidden: false,
99
+ interface: definition.interface,
100
+ schemaMetadata: {
101
+ ...compiled.schemaMetadata,
102
+ special: definition.special,
103
+ systemManaged: true,
104
+ },
105
+ });
106
+
107
+ if (field === 'created_by' || field === 'updated_by') {
108
+ constraints.push(
109
+ `CONSTRAINT ${quoteIdentifier(foreignKeyName(collection, field), 'system field constraint')} `
110
+ + `FOREIGN KEY (${quoteIdentifier(field, 'system field')}) REFERENCES yuncms_users (id) ON DELETE SET NULL`,
111
+ );
112
+ }
113
+ }
114
+
115
+ return { fields, columns, constraints, metadata };
116
+ }
117
+
118
+ function parseSchemaMetadata(value) {
119
+ if (value == null) return {};
120
+ if (typeof value === 'object') return value;
121
+ try {
122
+ return JSON.parse(value) ?? {};
123
+ } catch {
124
+ return {};
125
+ }
126
+ }
127
+
128
+ export function fieldSpecial(fieldSchema) {
129
+ return parseSchemaMetadata(fieldSchema?.schema_metadata).special ?? null;
130
+ }
131
+
132
+ export function isSystemManagedField(fieldSchema) {
133
+ return Boolean(fieldSpecial(fieldSchema));
134
+ }
135
+
136
+ export function systemMutationEntries(schema, accountability, operation, now = new Date()) {
137
+ const entries = [];
138
+ for (const fieldSchema of Object.values(schema?.fields ?? {})) {
139
+ const special = fieldSpecial(fieldSchema);
140
+ if (operation === 'create' && special === 'date-created') entries.push([fieldSchema.field, now]);
141
+ if ((operation === 'create' || operation === 'update') && special === 'date-updated') entries.push([fieldSchema.field, now]);
142
+ if (operation === 'create' && special === 'user-created') entries.push([fieldSchema.field, accountability?.user ?? null]);
143
+ if ((operation === 'create' || operation === 'update') && special === 'user-updated') entries.push([fieldSchema.field, accountability?.user ?? null]);
144
+ }
145
+ return entries;
146
+ }
@@ -0,0 +1,54 @@
1
+ const ALL_ACTIONS = Object.freeze(['read', 'create', 'update', 'delete']);
2
+
3
+ function parseMetadata(value) {
4
+ if (value == null) return {};
5
+ if (typeof value === 'object') return value;
6
+ try {
7
+ return JSON.parse(value) ?? {};
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+
13
+ function isEnabledFlag(value) {
14
+ return value === true || value === 1;
15
+ }
16
+
17
+ export function systemPermissionConfig(collectionSchema) {
18
+ if (!collectionSchema?.system) return null;
19
+ const metadata = parseMetadata(collectionSchema.metadata);
20
+ if (!isEnabledFlag(metadata.permissionManaged)) return null;
21
+ const allowedActions = Array.isArray(metadata.allowedActions)
22
+ ? metadata.allowedActions.filter((action) => ALL_ACTIONS.includes(action))
23
+ : [];
24
+ return Object.freeze({
25
+ resource: metadata.resource ?? collectionSchema.collection,
26
+ mode: metadata.permissionMode ?? 'action-only',
27
+ allowedActions: Object.freeze([...new Set(allowedActions)]),
28
+ });
29
+ }
30
+
31
+ export function isPermissionManagedSystemResource(collectionSchema) {
32
+ return Boolean(systemPermissionConfig(collectionSchema));
33
+ }
34
+
35
+ export function assertSystemResourceAction(collectionSchema, action) {
36
+ const config = systemPermissionConfig(collectionSchema);
37
+ if (!config) return null;
38
+ if (!config.allowedActions.includes(action)) {
39
+ const error = new Error(`Action ${action} is protected for system resource ${collectionSchema.collection}`);
40
+ error.code = 'SYSTEM_PERMISSION_ACTION_PROTECTED';
41
+ throw error;
42
+ }
43
+ return config;
44
+ }
45
+
46
+ export function assertActionOnlyPermissionPayload(collectionSchema, { fields, filter, validation } = {}) {
47
+ const config = systemPermissionConfig(collectionSchema);
48
+ if (!config || config.mode !== 'action-only') return;
49
+ if (fields != null || filter != null || validation != null) {
50
+ const error = new Error(`System resource ${collectionSchema.collection} supports action-level permissions only`);
51
+ error.code = 'SYSTEM_PERMISSION_ADVANCED_UNSUPPORTED';
52
+ throw error;
53
+ }
54
+ }