mtrl 0.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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/index.js +10 -0
- package/package.json +17 -0
- package/src/components/button/api.js +54 -0
- package/src/components/button/button.js +81 -0
- package/src/components/button/config.js +8 -0
- package/src/components/button/constants.js +63 -0
- package/src/components/button/index.js +2 -0
- package/src/components/button/styles.scss +231 -0
- package/src/components/checkbox/api.js +45 -0
- package/src/components/checkbox/checkbox.js +95 -0
- package/src/components/checkbox/constants.js +88 -0
- package/src/components/checkbox/index.js +2 -0
- package/src/components/checkbox/styles.scss +183 -0
- package/src/components/container/api.js +42 -0
- package/src/components/container/container.js +45 -0
- package/src/components/container/index.js +2 -0
- package/src/components/container/styles.scss +59 -0
- package/src/components/list/constants.js +89 -0
- package/src/components/list/index.js +2 -0
- package/src/components/list/list-item.js +147 -0
- package/src/components/list/list.js +267 -0
- package/src/components/list/styles/_list-item.scss +142 -0
- package/src/components/list/styles/_list.scss +89 -0
- package/src/components/list/styles/_variables.scss +13 -0
- package/src/components/list/styles.scss +19 -0
- package/src/components/navigation/api.js +43 -0
- package/src/components/navigation/constants.js +235 -0
- package/src/components/navigation/features/items.js +192 -0
- package/src/components/navigation/index.js +2 -0
- package/src/components/navigation/nav-item.js +137 -0
- package/src/components/navigation/navigation.js +55 -0
- package/src/components/navigation/styles/_bar.scss +51 -0
- package/src/components/navigation/styles/_base.scss +129 -0
- package/src/components/navigation/styles/_drawer.scss +169 -0
- package/src/components/navigation/styles/_rail.scss +65 -0
- package/src/components/navigation/styles.scss +6 -0
- package/src/components/snackbar/api.js +125 -0
- package/src/components/snackbar/constants.js +41 -0
- package/src/components/snackbar/features.js +69 -0
- package/src/components/snackbar/index.js +2 -0
- package/src/components/snackbar/position.js +63 -0
- package/src/components/snackbar/queue.js +74 -0
- package/src/components/snackbar/snackbar.js +70 -0
- package/src/components/snackbar/styles.scss +182 -0
- package/src/components/switch/api.js +44 -0
- package/src/components/switch/constants.js +80 -0
- package/src/components/switch/index.js +2 -0
- package/src/components/switch/styles.scss +172 -0
- package/src/components/switch/switch.js +71 -0
- package/src/components/textfield/api.js +49 -0
- package/src/components/textfield/constants.js +81 -0
- package/src/components/textfield/index.js +2 -0
- package/src/components/textfield/styles/base.scss +107 -0
- package/src/components/textfield/styles/filled.scss +58 -0
- package/src/components/textfield/styles/outlined.scss +66 -0
- package/src/components/textfield/styles.scss +6 -0
- package/src/components/textfield/textfield.js +68 -0
- package/src/core/build/constants.js +51 -0
- package/src/core/build/icon.js +78 -0
- package/src/core/build/ripple.js +92 -0
- package/src/core/build/text.js +54 -0
- package/src/core/collection/adapters/base.js +26 -0
- package/src/core/collection/adapters/mongodb.js +232 -0
- package/src/core/collection/adapters/route.js +201 -0
- package/src/core/collection/collection.js +259 -0
- package/src/core/collection/list-manager.js +157 -0
- package/src/core/compose/base.js +8 -0
- package/src/core/compose/component.js +225 -0
- package/src/core/compose/features/checkable.js +114 -0
- package/src/core/compose/features/disabled.js +25 -0
- package/src/core/compose/features/events.js +48 -0
- package/src/core/compose/features/icon.js +33 -0
- package/src/core/compose/features/index.js +20 -0
- package/src/core/compose/features/input.js +92 -0
- package/src/core/compose/features/lifecycle.js +69 -0
- package/src/core/compose/features/position.js +60 -0
- package/src/core/compose/features/ripple.js +32 -0
- package/src/core/compose/features/size.js +9 -0
- package/src/core/compose/features/style.js +12 -0
- package/src/core/compose/features/text.js +17 -0
- package/src/core/compose/features/textinput.js +118 -0
- package/src/core/compose/features/textlabel.js +28 -0
- package/src/core/compose/features/track.js +49 -0
- package/src/core/compose/features/variant.js +9 -0
- package/src/core/compose/features/withEvents.js +67 -0
- package/src/core/compose/index.js +16 -0
- package/src/core/compose/pipe.js +69 -0
- package/src/core/config.js +140 -0
- package/src/core/dom/attributes.js +33 -0
- package/src/core/dom/classes.js +70 -0
- package/src/core/dom/create.js +133 -0
- package/src/core/dom/events.js +175 -0
- package/src/core/dom/index.js +5 -0
- package/src/core/dom/utils.js +22 -0
- package/src/core/index.js +23 -0
- package/src/core/layout/index.js +93 -0
- package/src/core/state/disabled.js +14 -0
- package/src/core/state/emitter.js +63 -0
- package/src/core/state/events.js +29 -0
- package/src/core/state/index.js +6 -0
- package/src/core/state/lifecycle.js +64 -0
- package/src/core/state/store.js +112 -0
- package/src/core/utils/index.js +39 -0
- package/src/core/utils/mobile.js +74 -0
- package/src/core/utils/object.js +22 -0
- package/src/core/utils/validate.js +37 -0
- package/src/index.js +11 -0
- package/src/styles/abstract/_base.scss +2 -0
- package/src/styles/abstract/_config.scss +28 -0
- package/src/styles/abstract/_functions.scss +124 -0
- package/src/styles/abstract/_mixins.scss +261 -0
- package/src/styles/abstract/_variables.scss +158 -0
- package/src/styles/main.scss +78 -0
- package/src/styles/themes/_base-theme.scss +49 -0
- package/src/styles/themes/_baseline.scss +90 -0
- package/src/styles/themes/_forest.scss +71 -0
- package/src/styles/themes/_index.scss +6 -0
- package/src/styles/themes/_ocean.scss +71 -0
- package/src/styles/themes/_sunset.scss +55 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// src/core/collection/adapters/mongodb.js
|
|
2
|
+
|
|
3
|
+
import { MongoClient, ObjectId } from 'mongodb'
|
|
4
|
+
import { OPERATORS, createBaseAdapter } from './base'
|
|
5
|
+
|
|
6
|
+
const MONGODB_OPERATORS = {
|
|
7
|
+
[OPERATORS.EQ]: '$eq',
|
|
8
|
+
[OPERATORS.NE]: '$ne',
|
|
9
|
+
[OPERATORS.GT]: '$gt',
|
|
10
|
+
[OPERATORS.GTE]: '$gte',
|
|
11
|
+
[OPERATORS.LT]: '$lt',
|
|
12
|
+
[OPERATORS.LTE]: '$lte',
|
|
13
|
+
[OPERATORS.IN]: '$in',
|
|
14
|
+
[OPERATORS.NIN]: '$nin',
|
|
15
|
+
[OPERATORS.CONTAINS]: '$regex',
|
|
16
|
+
[OPERATORS.STARTS_WITH]: '$regex',
|
|
17
|
+
[OPERATORS.ENDS_WITH]: '$regex'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const createMongoAdapter = (config = {}) => {
|
|
21
|
+
const base = createBaseAdapter(config)
|
|
22
|
+
let client = null
|
|
23
|
+
let db = null
|
|
24
|
+
let collection = null
|
|
25
|
+
|
|
26
|
+
const transformDocument = (doc) => {
|
|
27
|
+
if (!doc) return null
|
|
28
|
+
const { _id, ...rest } = doc
|
|
29
|
+
return { id: _id.toString(), ...rest }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const transformForMongo = (doc) => {
|
|
33
|
+
if (!doc) return null
|
|
34
|
+
const { id, ...rest } = doc
|
|
35
|
+
return { _id: id ? new ObjectId(id) : new ObjectId(), ...rest }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const transformQuery = (query) => {
|
|
39
|
+
const transformed = {}
|
|
40
|
+
|
|
41
|
+
Object.entries(query).forEach(([field, conditions]) => {
|
|
42
|
+
if (typeof conditions === 'object') {
|
|
43
|
+
transformed[field] = Object.entries(conditions).reduce((acc, [op, value]) => {
|
|
44
|
+
const mongoOp = MONGODB_OPERATORS[op]
|
|
45
|
+
if (!mongoOp) return acc
|
|
46
|
+
|
|
47
|
+
if (op === OPERATORS.CONTAINS) {
|
|
48
|
+
acc[mongoOp] = new RegExp(value, 'i')
|
|
49
|
+
} else if (op === OPERATORS.STARTS_WITH) {
|
|
50
|
+
acc[mongoOp] = new RegExp(`^${value}`, 'i')
|
|
51
|
+
} else if (op === OPERATORS.ENDS_WITH) {
|
|
52
|
+
acc[mongoOp] = new RegExp(`${value}$`, 'i')
|
|
53
|
+
} else {
|
|
54
|
+
acc[mongoOp] = value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return acc
|
|
58
|
+
}, {})
|
|
59
|
+
} else {
|
|
60
|
+
transformed[field] = conditions
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return transformed
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...base,
|
|
69
|
+
|
|
70
|
+
connect: async () => {
|
|
71
|
+
try {
|
|
72
|
+
client = new MongoClient(config.uri, {
|
|
73
|
+
useUnifiedTopology: true,
|
|
74
|
+
maxPoolSize: 10,
|
|
75
|
+
...config.options
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await client.connect()
|
|
79
|
+
db = client.db(config.dbName)
|
|
80
|
+
collection = db.collection(config.collection)
|
|
81
|
+
|
|
82
|
+
// Optional: Create indexes
|
|
83
|
+
// await collection.createIndex({ field: 1 })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return base.handleError(new Error(`MongoDB connection failed: ${error.message}`))
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
disconnect: async () => {
|
|
90
|
+
if (client) {
|
|
91
|
+
await client.close()
|
|
92
|
+
client = null
|
|
93
|
+
db = null
|
|
94
|
+
collection = null
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
create: async (items) => {
|
|
99
|
+
if (!collection) {
|
|
100
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const docs = items.map(item => transformForMongo(item))
|
|
104
|
+
const result = await collection.insertMany(docs)
|
|
105
|
+
|
|
106
|
+
return items.map((item, index) => ({
|
|
107
|
+
...item,
|
|
108
|
+
id: result.insertedIds[index].toString()
|
|
109
|
+
}))
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
read: async (query = {}, options = {}) => {
|
|
113
|
+
if (!collection) {
|
|
114
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const {
|
|
118
|
+
skip = 0,
|
|
119
|
+
limit = 0,
|
|
120
|
+
sort = {},
|
|
121
|
+
projection = {}
|
|
122
|
+
} = options
|
|
123
|
+
|
|
124
|
+
const mongoQuery = transformQuery(query)
|
|
125
|
+
|
|
126
|
+
const cursor = collection
|
|
127
|
+
.find(mongoQuery)
|
|
128
|
+
.skip(skip)
|
|
129
|
+
.limit(limit)
|
|
130
|
+
.project(projection)
|
|
131
|
+
.sort(sort)
|
|
132
|
+
|
|
133
|
+
const docs = await cursor.toArray()
|
|
134
|
+
return docs.map(doc => transformDocument(doc))
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
update: async (items) => {
|
|
138
|
+
if (!collection) {
|
|
139
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const operations = items.map(item => ({
|
|
143
|
+
updateOne: {
|
|
144
|
+
filter: { _id: new ObjectId(item.id) },
|
|
145
|
+
update: { $set: transformForMongo(item) },
|
|
146
|
+
upsert: false
|
|
147
|
+
}
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
await collection.bulkWrite(operations)
|
|
151
|
+
return items
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
delete: async (ids) => {
|
|
155
|
+
if (!collection) {
|
|
156
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const mongoIds = ids.map(id => new ObjectId(id))
|
|
160
|
+
await collection.deleteMany({ _id: { $in: mongoIds } })
|
|
161
|
+
return ids
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
query: async (query = {}, options = {}) => {
|
|
165
|
+
if (!collection) {
|
|
166
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const {
|
|
170
|
+
pipeline = [],
|
|
171
|
+
skip = 0,
|
|
172
|
+
limit = 0
|
|
173
|
+
} = options
|
|
174
|
+
|
|
175
|
+
const mongoQuery = transformQuery(query)
|
|
176
|
+
|
|
177
|
+
const aggregation = [
|
|
178
|
+
{ $match: mongoQuery },
|
|
179
|
+
...pipeline
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
if (skip > 0) {
|
|
183
|
+
aggregation.push({ $skip: skip })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (limit > 0) {
|
|
187
|
+
aggregation.push({ $limit: limit })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const docs = await collection.aggregate(aggregation).toArray()
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
results: docs.map(doc => transformDocument(doc)),
|
|
194
|
+
total: await collection.countDocuments(mongoQuery)
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
watch: (callback, pipeline = []) => {
|
|
199
|
+
if (!collection) {
|
|
200
|
+
return base.handleError(new Error('Not connected to MongoDB'))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const changeStream = collection.watch(pipeline, {
|
|
204
|
+
fullDocument: 'updateLookup'
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
changeStream.on('change', async (change) => {
|
|
208
|
+
const { operationType, documentKey, fullDocument } = change
|
|
209
|
+
|
|
210
|
+
switch (operationType) {
|
|
211
|
+
case 'insert':
|
|
212
|
+
case 'update':
|
|
213
|
+
case 'replace':
|
|
214
|
+
callback({
|
|
215
|
+
type: operationType,
|
|
216
|
+
document: transformDocument(fullDocument)
|
|
217
|
+
})
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
case 'delete':
|
|
221
|
+
callback({
|
|
222
|
+
type: operationType,
|
|
223
|
+
documentId: documentKey._id.toString()
|
|
224
|
+
})
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return () => changeStream.close()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/core/collection/adapters/route.js
|
|
2
|
+
|
|
3
|
+
import { OPERATORS, createBaseAdapter } from './base'
|
|
4
|
+
|
|
5
|
+
const QUERY_PARAMS = {
|
|
6
|
+
[OPERATORS.EQ]: 'eq',
|
|
7
|
+
[OPERATORS.NE]: 'ne',
|
|
8
|
+
[OPERATORS.GT]: 'gt',
|
|
9
|
+
[OPERATORS.GTE]: 'gte',
|
|
10
|
+
[OPERATORS.LT]: 'lt',
|
|
11
|
+
[OPERATORS.LTE]: 'lte',
|
|
12
|
+
[OPERATORS.IN]: 'in',
|
|
13
|
+
[OPERATORS.NIN]: 'nin',
|
|
14
|
+
[OPERATORS.CONTAINS]: 'contains',
|
|
15
|
+
[OPERATORS.STARTS_WITH]: 'startsWith',
|
|
16
|
+
[OPERATORS.ENDS_WITH]: 'endsWith'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const createRouteAdapter = (config = {}) => {
|
|
20
|
+
const base = createBaseAdapter(config)
|
|
21
|
+
let controller = null
|
|
22
|
+
const cache = new Map()
|
|
23
|
+
|
|
24
|
+
const buildUrl = (endpoint, params = {}) => {
|
|
25
|
+
const url = new URL(config.base + endpoint)
|
|
26
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
value.forEach(v => url.searchParams.append(key, v))
|
|
29
|
+
} else if (value !== null && value !== undefined) {
|
|
30
|
+
url.searchParams.append(key, value)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
return url.toString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const transformQuery = (query) => {
|
|
37
|
+
const params = {}
|
|
38
|
+
Object.entries(query).forEach(([field, conditions]) => {
|
|
39
|
+
if (typeof conditions === 'object') {
|
|
40
|
+
Object.entries(conditions).forEach(([op, value]) => {
|
|
41
|
+
const paramKey = QUERY_PARAMS[op]
|
|
42
|
+
if (paramKey) {
|
|
43
|
+
params[`${field}_${paramKey}`] = value
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
} else {
|
|
47
|
+
params[field] = conditions
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
return params
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const request = async (url, options = {}) => {
|
|
54
|
+
if (controller) {
|
|
55
|
+
controller.abort()
|
|
56
|
+
}
|
|
57
|
+
controller = new AbortController()
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
...options,
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
...config.headers,
|
|
65
|
+
...options.headers
|
|
66
|
+
},
|
|
67
|
+
signal: controller.signal
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const error = await response.json()
|
|
72
|
+
return base.handleError(new Error(error.message || 'API request failed'))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return await response.json()
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.name === 'AbortError') return null
|
|
78
|
+
return base.handleError(error)
|
|
79
|
+
} finally {
|
|
80
|
+
controller = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getCache = (key) => {
|
|
85
|
+
if (!config.cache) return null
|
|
86
|
+
|
|
87
|
+
const cached = cache.get(key)
|
|
88
|
+
if (!cached) return null
|
|
89
|
+
|
|
90
|
+
const { data, timestamp } = cached
|
|
91
|
+
const now = Date.now()
|
|
92
|
+
|
|
93
|
+
if (now - timestamp > 5 * 60 * 1000) {
|
|
94
|
+
cache.delete(key)
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return data
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const setCache = (key, data) => {
|
|
102
|
+
if (!config.cache) return
|
|
103
|
+
cache.set(key, { data, timestamp: Date.now() })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
|
|
109
|
+
create: async (items) => {
|
|
110
|
+
const url = buildUrl(config.endpoints.create)
|
|
111
|
+
const response = await request(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify({ items })
|
|
114
|
+
})
|
|
115
|
+
return response.items
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
read: async (query = {}, options = {}) => {
|
|
119
|
+
const {
|
|
120
|
+
page = 1,
|
|
121
|
+
limit = 20,
|
|
122
|
+
sort,
|
|
123
|
+
fields
|
|
124
|
+
} = options
|
|
125
|
+
|
|
126
|
+
const params = {
|
|
127
|
+
...transformQuery(query),
|
|
128
|
+
page,
|
|
129
|
+
limit,
|
|
130
|
+
sort,
|
|
131
|
+
fields
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const url = buildUrl(config.endpoints.list, params)
|
|
135
|
+
const cacheKey = url.toString()
|
|
136
|
+
|
|
137
|
+
const cached = getCache(cacheKey)
|
|
138
|
+
if (cached) return cached
|
|
139
|
+
|
|
140
|
+
const response = await request(url)
|
|
141
|
+
setCache(cacheKey, response.items)
|
|
142
|
+
|
|
143
|
+
return response.items
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
update: async (items) => {
|
|
147
|
+
const url = buildUrl(config.endpoints.update)
|
|
148
|
+
const response = await request(url, {
|
|
149
|
+
method: 'PUT',
|
|
150
|
+
body: JSON.stringify({ items })
|
|
151
|
+
})
|
|
152
|
+
return response.items
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
delete: async (ids) => {
|
|
156
|
+
const url = buildUrl(config.endpoints.delete)
|
|
157
|
+
const response = await request(url, {
|
|
158
|
+
method: 'DELETE',
|
|
159
|
+
body: JSON.stringify({ ids })
|
|
160
|
+
})
|
|
161
|
+
return response.ids
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
query: async (query = {}, options = {}) => {
|
|
165
|
+
const {
|
|
166
|
+
page = 1,
|
|
167
|
+
limit = 20,
|
|
168
|
+
sort,
|
|
169
|
+
fields,
|
|
170
|
+
...rest
|
|
171
|
+
} = options
|
|
172
|
+
|
|
173
|
+
const params = {
|
|
174
|
+
...transformQuery(query),
|
|
175
|
+
...rest,
|
|
176
|
+
page,
|
|
177
|
+
limit,
|
|
178
|
+
sort,
|
|
179
|
+
fields
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const url = buildUrl(config.endpoints.list, params)
|
|
183
|
+
const response = await request(url)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
items: response.items,
|
|
187
|
+
total: response.total,
|
|
188
|
+
page: response.page,
|
|
189
|
+
pages: response.pages
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
disconnect: () => {
|
|
194
|
+
if (controller) {
|
|
195
|
+
controller.abort()
|
|
196
|
+
controller = null
|
|
197
|
+
}
|
|
198
|
+
cache.clear()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// src/core/collection/collection.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event types for collection changes
|
|
5
|
+
*/
|
|
6
|
+
export const COLLECTION_EVENTS = {
|
|
7
|
+
CHANGE: 'change',
|
|
8
|
+
ADD: 'add',
|
|
9
|
+
UPDATE: 'update',
|
|
10
|
+
REMOVE: 'remove',
|
|
11
|
+
ERROR: 'error',
|
|
12
|
+
LOADING: 'loading'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Query operators for filtering
|
|
17
|
+
*/
|
|
18
|
+
export const OPERATORS = {
|
|
19
|
+
EQ: 'eq',
|
|
20
|
+
NE: 'ne',
|
|
21
|
+
GT: 'gt',
|
|
22
|
+
GTE: 'gte',
|
|
23
|
+
LT: 'lt',
|
|
24
|
+
LTE: 'lte',
|
|
25
|
+
IN: 'in',
|
|
26
|
+
NIN: 'nin',
|
|
27
|
+
CONTAINS: 'contains',
|
|
28
|
+
STARTS_WITH: 'startsWith',
|
|
29
|
+
ENDS_WITH: 'endsWith'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base Collection class providing data management interface
|
|
34
|
+
* @template T - Type of items in collection
|
|
35
|
+
*/
|
|
36
|
+
export class Collection {
|
|
37
|
+
#items = new Map()
|
|
38
|
+
#observers = new Set()
|
|
39
|
+
#query = null
|
|
40
|
+
#sort = null
|
|
41
|
+
#loading = false
|
|
42
|
+
#error = null
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new collection instance
|
|
46
|
+
* @param {Object} config - Collection configuration
|
|
47
|
+
* @param {Function} config.transform - Transform function for items
|
|
48
|
+
* @param {Function} config.validate - Validation function for items
|
|
49
|
+
*/
|
|
50
|
+
constructor (config = {}) {
|
|
51
|
+
this.transform = config.transform || (item => item)
|
|
52
|
+
this.validate = config.validate || (() => true)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to collection changes
|
|
57
|
+
* @param {Function} observer - Observer callback
|
|
58
|
+
* @returns {Function} Unsubscribe function
|
|
59
|
+
*/
|
|
60
|
+
subscribe (observer) {
|
|
61
|
+
this.#observers.add(observer)
|
|
62
|
+
return () => this.#observers.delete(observer)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Notify observers of collection changes
|
|
67
|
+
* @param {string} event - Event type
|
|
68
|
+
* @param {*} data - Event data
|
|
69
|
+
*/
|
|
70
|
+
#notify (event, data) {
|
|
71
|
+
this.#observers.forEach(observer => observer({ event, data }))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set loading state
|
|
76
|
+
* @param {boolean} loading - Loading state
|
|
77
|
+
*/
|
|
78
|
+
#setLoading (loading) {
|
|
79
|
+
this.#loading = loading
|
|
80
|
+
this.#notify(COLLECTION_EVENTS.LOADING, loading)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set error state
|
|
85
|
+
* @param {Error} error - Error object
|
|
86
|
+
*/
|
|
87
|
+
#setError (error) {
|
|
88
|
+
this.#error = error
|
|
89
|
+
this.#notify(COLLECTION_EVENTS.ERROR, error)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get collection items based on current query and sort
|
|
94
|
+
* @returns {Array<T>} Collection items
|
|
95
|
+
*/
|
|
96
|
+
get items () {
|
|
97
|
+
let result = Array.from(this.#items.values())
|
|
98
|
+
|
|
99
|
+
if (this.#query) {
|
|
100
|
+
result = result.filter(this.#query)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.#sort) {
|
|
104
|
+
result.sort(this.#sort)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get collection size
|
|
112
|
+
* @returns {number} Number of items
|
|
113
|
+
*/
|
|
114
|
+
get size () {
|
|
115
|
+
return this.#items.size
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get loading state
|
|
120
|
+
* @returns {boolean} Loading state
|
|
121
|
+
*/
|
|
122
|
+
get loading () {
|
|
123
|
+
return this.#loading
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get error state
|
|
128
|
+
* @returns {Error|null} Error object
|
|
129
|
+
*/
|
|
130
|
+
get error () {
|
|
131
|
+
return this.#error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Set query filter
|
|
136
|
+
* @param {Function} queryFn - Query function
|
|
137
|
+
*/
|
|
138
|
+
query (queryFn) {
|
|
139
|
+
this.#query = queryFn
|
|
140
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set sort function
|
|
145
|
+
* @param {Function} sortFn - Sort function
|
|
146
|
+
*/
|
|
147
|
+
sort (sortFn) {
|
|
148
|
+
this.#sort = sortFn
|
|
149
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Add items to collection
|
|
154
|
+
* @param {T|Array<T>} items - Items to add
|
|
155
|
+
* @returns {Promise<Array<T>>} Added items
|
|
156
|
+
*/
|
|
157
|
+
async add (items) {
|
|
158
|
+
try {
|
|
159
|
+
this.#setLoading(true)
|
|
160
|
+
const toAdd = Array.isArray(items) ? items : [items]
|
|
161
|
+
|
|
162
|
+
const validated = toAdd.filter(this.validate)
|
|
163
|
+
const transformed = validated.map(this.transform)
|
|
164
|
+
|
|
165
|
+
transformed.forEach(item => {
|
|
166
|
+
if (!item.id) {
|
|
167
|
+
throw new Error('Items must have an id property')
|
|
168
|
+
}
|
|
169
|
+
this.#items.set(item.id, item)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
this.#notify(COLLECTION_EVENTS.ADD, transformed)
|
|
173
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
174
|
+
|
|
175
|
+
return transformed
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.#setError(error)
|
|
178
|
+
throw error
|
|
179
|
+
} finally {
|
|
180
|
+
this.#setLoading(false)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Update items in collection
|
|
186
|
+
* @param {T|Array<T>} items - Items to update
|
|
187
|
+
* @returns {Promise<Array<T>>} Updated items
|
|
188
|
+
*/
|
|
189
|
+
async update (items) {
|
|
190
|
+
try {
|
|
191
|
+
this.#setLoading(true)
|
|
192
|
+
const toUpdate = Array.isArray(items) ? items : [items]
|
|
193
|
+
|
|
194
|
+
const updated = toUpdate.map(item => {
|
|
195
|
+
if (!this.#items.has(item.id)) {
|
|
196
|
+
throw new Error(`Item with id ${item.id} not found`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const validated = this.validate(item)
|
|
200
|
+
if (!validated) {
|
|
201
|
+
throw new Error(`Invalid item: ${item.id}`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const transformed = this.transform(item)
|
|
205
|
+
this.#items.set(item.id, transformed)
|
|
206
|
+
return transformed
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
this.#notify(COLLECTION_EVENTS.UPDATE, updated)
|
|
210
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
211
|
+
|
|
212
|
+
return updated
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.#setError(error)
|
|
215
|
+
throw error
|
|
216
|
+
} finally {
|
|
217
|
+
this.#setLoading(false)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Remove items from collection
|
|
223
|
+
* @param {string|Array<string>} ids - Item IDs to remove
|
|
224
|
+
* @returns {Promise<Array<string>>} Removed item IDs
|
|
225
|
+
*/
|
|
226
|
+
async remove (ids) {
|
|
227
|
+
try {
|
|
228
|
+
this.#setLoading(true)
|
|
229
|
+
const toRemove = Array.isArray(ids) ? ids : [ids]
|
|
230
|
+
|
|
231
|
+
toRemove.forEach(id => {
|
|
232
|
+
if (!this.#items.has(id)) {
|
|
233
|
+
throw new Error(`Item with id ${id} not found`)
|
|
234
|
+
}
|
|
235
|
+
this.#items.delete(id)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
this.#notify(COLLECTION_EVENTS.REMOVE, toRemove)
|
|
239
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
240
|
+
|
|
241
|
+
return toRemove
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.#setError(error)
|
|
244
|
+
throw error
|
|
245
|
+
} finally {
|
|
246
|
+
this.#setLoading(false)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clear all items from collection
|
|
252
|
+
*/
|
|
253
|
+
clear () {
|
|
254
|
+
this.#items.clear()
|
|
255
|
+
this.#query = null
|
|
256
|
+
this.#sort = null
|
|
257
|
+
this.#notify(COLLECTION_EVENTS.CHANGE, this.items)
|
|
258
|
+
}
|
|
259
|
+
}
|