metaowl 0.4.0 → 0.5.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
package/modules/meta.js DELETED
@@ -1,119 +0,0 @@
1
- /**
2
- * @module Meta
3
- *
4
- * Programmatic helpers for managing document meta tags at runtime.
5
- *
6
- * Each function is idempotent: the relevant `<meta>` or `<link>` element is
7
- * created on first call if it does not already exist, then its content is
8
- * updated on every subsequent call as well.
9
- *
10
- * Import the entire namespace via:
11
- * import { Meta } from 'metaowl'
12
- * Meta.title('My Page')
13
- */
14
-
15
- function _nameMeta(name, value) {
16
- if (!value) return
17
- let el = document.querySelector(`meta[name="${name}"]`)
18
- if (!el) {
19
- el = document.createElement('meta')
20
- el.name = name
21
- document.head.appendChild(el)
22
- }
23
- el.content = value
24
- }
25
-
26
- function _propMeta(property, value) {
27
- if (!value) return
28
- let el = document.querySelector(`meta[property="${property}"]`)
29
- if (!el) {
30
- el = document.createElement('meta')
31
- el.setAttribute('property', property)
32
- document.head.appendChild(el)
33
- }
34
- el.content = value
35
- }
36
-
37
- /** Set the document `<title>`. @param {string} title */
38
- export function title(title) {
39
- if (!title) return
40
- document.title = title
41
- }
42
-
43
- /** @param {string} description */
44
- export function description(description) { _nameMeta('description', description) }
45
-
46
- /** @param {string} keywords */
47
- export function keywords(keywords) { _nameMeta('keywords', keywords) }
48
-
49
- /** @param {string} author */
50
- export function author(author) { _nameMeta('author', author) }
51
-
52
- /** @param {string} url */
53
- export function canonical(url) {
54
- if (!url) return
55
- let el = document.querySelector('link[rel="canonical"]')
56
- if (!el) {
57
- el = document.createElement('link')
58
- el.rel = 'canonical'
59
- document.head.appendChild(el)
60
- }
61
- el.href = url
62
- }
63
-
64
- /** @param {string} title */
65
- export function ogTitle(title) { _propMeta('og:title', title) }
66
-
67
- /** @param {string} description */
68
- export function ogDescription(description) { _propMeta('og:description', description) }
69
-
70
- /** @param {string} image */
71
- export function ogImage(image) { _propMeta('og:image', image) }
72
-
73
- /** @param {string} url */
74
- export function ogUrl(url) { _propMeta('og:url', url) }
75
-
76
- /** @param {string} type */
77
- export function ogType(type) { _propMeta('og:type', type) }
78
-
79
- /** @param {string} siteName */
80
- export function ogSiteName(siteName) { _propMeta('og:site_name', siteName) }
81
-
82
- /** @param {string} locale */
83
- export function ogLocale(locale) { _propMeta('og:locale', locale) }
84
-
85
- /** @param {string|number} width */
86
- export function ogImageWidth(width) { _propMeta('og:image:width', width) }
87
-
88
- /** @param {string|number} height */
89
- export function ogImageHeight(height) { _propMeta('og:image:height', height) }
90
-
91
- /** @param {string} card */
92
- export function twitterCard(card) { _nameMeta('twitter:card', card) }
93
-
94
- /** @param {string} site */
95
- export function twitterSite(site) { _nameMeta('twitter:site', site) }
96
-
97
- /** @param {string} creator */
98
- export function twitterCreator(creator) { _nameMeta('twitter:creator', creator) }
99
-
100
- /** @param {string} title */
101
- export function twitterTitle(title) { _nameMeta('twitter:title', title) }
102
-
103
- /** @param {string} description */
104
- export function twitterDescription(description) { _nameMeta('twitter:description', description) }
105
-
106
- /** @param {string} image */
107
- export function twitterImage(image) { _nameMeta('twitter:image', image) }
108
-
109
- /** @param {string} alt */
110
- export function twitterImageAlt(alt) { _nameMeta('twitter:image:alt', alt) }
111
-
112
- /** @param {string} url */
113
- export function twitterUrl(url) { _nameMeta('twitter:url', url) }
114
-
115
- /** @param {string} siteId */
116
- export function twitterSiteId(siteId) { _nameMeta('twitter:site:id', siteId) }
117
-
118
- /** @param {string} creatorId */
119
- export function twitterCreatorId(creatorId) { _nameMeta('twitter:creator:id', creatorId) }
@@ -1,511 +0,0 @@
1
- /**
2
- * @module OdooRPC
3
- *
4
- * Odoo JSON-RPC Service for MetaOwl applications.
5
- *
6
- * Features:
7
- * - JSON-RPC 2.0 compliant communication
8
- * - Authentication handling
9
- * - Automatic CSRF token management
10
- * - Session persistence
11
- * - Common Odoo operations (search_read, call, etc.)
12
- *
13
- * Usage:
14
- * import { OdooService } from 'metaowl'
15
- *
16
- * // Configure connection
17
- * OdooService.configure({
18
- * baseUrl: 'https://my-odoo-instance.com',
19
- * database: 'my_database',
20
- * username: 'admin',
21
- * password: 'admin'
22
- * })
23
- *
24
- * // Authenticate
25
- * await OdooService.authenticate()
26
- *
27
- * // Search and read records
28
- * const partners = await OdooService.searchRead('res.partner', {
29
- * domain: [['is_company', '=', true]],
30
- * fields: ['name', 'email'],
31
- * limit: 10
32
- * })
33
- *
34
- * // Call any model method
35
- * const result = await OdooService.call('res.partner', 'create', [{
36
- * name: 'New Partner',
37
- * email: 'partner@example.com'
38
- * }])
39
- */
40
-
41
- import Fetch from './fetch.js'
42
-
43
- /**
44
- * @typedef {Object} OdooConfig
45
- * @property {string} baseUrl - Odoo instance URL
46
- * @property {string} database - Database name
47
- * @property {string} [username] - Username for authentication
48
- * @property {string} [password] - Password for authentication
49
- * @property {string} [apiKey] - API key for authentication (alternative to password)
50
- * @property {boolean} [persistSession=true] - Persist session in localStorage
51
- */
52
-
53
- /**
54
- * @typedef {Object} SearchReadOptions
55
- * @property {Array[]} [domain=[]] - Search domain
56
- * @property {string[]} [fields=[]] - Fields to read
57
- * @property {number} [limit=80] - Max records
58
- * @property {number} [offset=0] - Offset for pagination
59
- * @property {string} [order] - Order by clause
60
- * @property {Object} [context={}] - Odoo context
61
- */
62
-
63
- /**
64
- * @typedef {Object} OdooSession
65
- * @property {number} uid - User ID
66
- * @property {string} username - Username
67
- * @property {string} [name] - Display name
68
- * @property {number} [partner_id] - Partner ID
69
- * @property {string[]} [user_context] - User context
70
- */
71
-
72
- /** @type {OdooConfig|null} */
73
- let _config = null
74
-
75
- /** @type {OdooSession|null} */
76
- let _session = null
77
-
78
- /** @type {string|null} */
79
- let _csrfToken = null
80
-
81
- /** @type {Function[]} */
82
- const _authListeners = []
83
-
84
- const SESSION_KEY = 'metaowl:odoo:session'
85
- const CSRF_KEY = 'metaowl:odoo:csrf'
86
-
87
- /**
88
- * Configure the Odoo RPC service.
89
- *
90
- * @param {OdooConfig} config
91
- */
92
- export function configure(config) {
93
- _config = {
94
- persistSession: true,
95
- ...config
96
- }
97
-
98
- // Try to restore session from localStorage
99
- if (_config.persistSession) {
100
- restoreSession()
101
- }
102
- }
103
-
104
- /**
105
- * Get current configuration.
106
- *
107
- * @returns {OdooConfig|null}
108
- */
109
- export function getConfig() {
110
- return _config
111
- }
112
-
113
- /**
114
- * Check if service is configured.
115
- *
116
- * @returns {boolean}
117
- */
118
- export function isConfigured() {
119
- return _config !== null && !!_config.baseUrl && !!_config.database
120
- }
121
-
122
- /**
123
- * Restore session from localStorage.
124
- */
125
- function restoreSession() {
126
- try {
127
- const sessionData = localStorage.getItem(SESSION_KEY)
128
- const csrfData = localStorage.getItem(CSRF_KEY)
129
-
130
- if (sessionData) {
131
- _session = JSON.parse(sessionData)
132
- }
133
- if (csrfData) {
134
- _csrfToken = csrfData
135
- }
136
- } catch {
137
- // Ignore storage errors
138
- }
139
- }
140
-
141
- /**
142
- * Save session to localStorage.
143
- */
144
- function saveSession() {
145
- if (!_config?.persistSession) return
146
-
147
- try {
148
- if (_session) {
149
- localStorage.setItem(SESSION_KEY, JSON.stringify(_session))
150
- } else {
151
- localStorage.removeItem(SESSION_KEY)
152
- }
153
- if (_csrfToken) {
154
- localStorage.setItem(CSRF_KEY, _csrfToken)
155
- } else {
156
- localStorage.removeItem(CSRF_KEY)
157
- }
158
- } catch {
159
- // Ignore storage errors
160
- }
161
- }
162
-
163
- /**
164
- * Make JSON-RPC request to Odoo.
165
- *
166
- * @param {string} service - Service name (e.g., 'common', 'object', 'db')
167
- @param {string} method - Method name
168
- * @param {any[]} args - Method arguments
169
- * @returns {Promise<any>}
170
- * @throws {Error}
171
- */
172
- async function jsonRpc(service, method, args = []) {
173
- if (!isConfigured()) {
174
- throw new Error('[metaowl] OdooService not configured. Call configure() first.')
175
- }
176
-
177
- const url = `${_config.baseUrl}/jsonrpc`
178
-
179
- const payload = {
180
- jsonrpc: '2.0',
181
- method: 'call',
182
- params: {
183
- service,
184
- method,
185
- args
186
- },
187
- id: Math.floor(Math.random() * 1000000000)
188
- }
189
-
190
- const headers = {
191
- 'Content-Type': 'application/json'
192
- }
193
-
194
- if (_csrfToken) {
195
- headers['X-CSRF-Token'] = _csrfToken
196
- }
197
-
198
- const response = await fetch(url, {
199
- method: 'POST',
200
- headers,
201
- body: JSON.stringify(payload),
202
- credentials: 'include'
203
- })
204
-
205
- if (!response.ok) {
206
- throw new Error(`[metaowl] HTTP ${response.status}: ${response.statusText}`)
207
- }
208
-
209
- const data = await response.json()
210
-
211
- if (data.error) {
212
- const error = data.error
213
- throw new Error(`[metaowl] Odoo Error: ${error.message || error.data?.message || JSON.stringify(error)}`)
214
- }
215
-
216
- // Extract CSRF token from cookies if present
217
- const setCookie = response.headers.get('set-cookie')
218
- if (setCookie?.includes('csrf_token')) {
219
- const match = setCookie.match(/csrf_token=([^;]+)/)
220
- if (match) {
221
- _csrfToken = match[1]
222
- saveSession()
223
- }
224
- }
225
-
226
- return data.result
227
- }
228
-
229
- /**
230
- * Authenticate with Odoo.
231
- *
232
- * @param {string} [username] - Override configured username
233
- * @param {string} [password] - Override configured password
234
- * @returns {Promise<OdooSession>} Session info
235
- * @throws {Error}
236
- */
237
- export async function authenticate(username, password) {
238
- const user = username || _config?.username
239
- const pass = password || _config?.password || _config?.apiKey
240
-
241
- if (!user || !pass) {
242
- throw new Error('[metaowl] Authentication requires username and password/apiKey')
243
- }
244
-
245
- const uid = await jsonRpc('common', 'authenticate', [
246
- _config.database,
247
- user,
248
- pass,
249
- {}
250
- ])
251
-
252
- if (!uid) {
253
- throw new Error('[metaowl] Authentication failed: invalid credentials')
254
- }
255
-
256
- _session = {
257
- uid,
258
- username: user
259
- }
260
-
261
- // Get user info
262
- try {
263
- const userInfo = await searchRead('res.users', {
264
- domain: [['id', '=', uid]],
265
- fields: ['name', 'partner_id', 'lang', 'tz'],
266
- limit: 1
267
- })
268
-
269
- if (userInfo.length > 0) {
270
- _session.name = userInfo[0].name
271
- _session.partner_id = userInfo[0].partner_id?.[0]
272
- _session.lang = userInfo[0].lang
273
- _session.tz = userInfo[0].tz
274
- }
275
- } catch {
276
- // Ignore user info fetch errors
277
- }
278
-
279
- saveSession()
280
- notifyAuthListeners()
281
-
282
- return _session
283
- }
284
-
285
- /**
286
- * Check if currently authenticated.
287
- *
288
- * @returns {boolean}
289
- */
290
- export function isAuthenticated() {
291
- return _session !== null && _session.uid !== null
292
- }
293
-
294
- /**
295
- * Get current session.
296
- *
297
- * @returns {OdooSession|null}
298
- */
299
- export function getSession() {
300
- return _session
301
- }
302
-
303
- /**
304
- * Logout and clear session.
305
- */
306
- export function logout() {
307
- _session = null
308
- _csrfToken = null
309
- saveSession()
310
- notifyAuthListeners()
311
- }
312
-
313
- /**
314
- * Search and read records from Odoo.
315
- *
316
- * @param {string} model - Model name (e.g., 'res.partner')
317
- * @param {SearchReadOptions} options - Search options
318
- * @returns {Promise<Object[]>} Records
319
- */
320
- export async function searchRead(model, options = {}) {
321
- const {
322
- domain = [],
323
- fields = [],
324
- limit = 80,
325
- offset = 0,
326
- order = '',
327
- context = {}
328
- } = options
329
-
330
- if (!isAuthenticated()) {
331
- throw new Error('[metaowl] Not authenticated. Call authenticate() first.')
332
- }
333
-
334
- const args = [
335
- _config.database,
336
- _session.uid,
337
- _config.password || _config.apiKey,
338
- model,
339
- 'search_read',
340
- [domain],
341
- { fields, limit, offset, order, context }
342
- ]
343
-
344
- return await jsonRpc('object', 'execute_kw', args)
345
- }
346
-
347
- /**
348
- * Call any model method.
349
- *
350
- * @param {string} model - Model name
351
- * @param {string} method - Method name
352
- * @param {any[]} [args=[]] - Positional arguments
353
- * @param {Object} [kwargs={}] - Keyword arguments
354
- * @returns {Promise<any>}
355
- */
356
- export async function call(model, method, args = [], kwargs = {}) {
357
- if (!isAuthenticated()) {
358
- throw new Error('[metaowl] Not authenticated. Call authenticate() first.')
359
- }
360
-
361
- const rpcArgs = [
362
- _config.database,
363
- _session.uid,
364
- _config.password || _config.apiKey,
365
- model,
366
- method,
367
- args,
368
- kwargs
369
- ]
370
-
371
- return await jsonRpc('object', 'execute_kw', rpcArgs)
372
- }
373
-
374
- /**
375
- * Read specific records by ID.
376
- *
377
- * @param {string} model - Model name
378
- * @param {number[]} ids - Record IDs
379
- * @param {string[]} [fields=[]] - Fields to read
380
- * @returns {Promise<Object[]>}
381
- */
382
- export async function read(model, ids, fields = []) {
383
- return await call(model, 'read', [ids], { fields })
384
- }
385
-
386
- /**
387
- * Create a new record.
388
- *
389
- * @param {string} model - Model name
390
- * @param {Object} values - Field values
391
- * @returns {Promise<number>} New record ID
392
- */
393
- export async function create(model, values) {
394
- return await call(model, 'create', [[values]])
395
- }
396
-
397
- /**
398
- * Update existing records.
399
- *
400
- * @param {string} model - Model name
401
- * @param {number[]} ids - Record IDs to update
402
- * @param {Object} values - New field values
403
- * @returns {Promise<boolean>}
404
- */
405
- export async function write(model, ids, values) {
406
- return await call(model, 'write', [ids, values])
407
- }
408
-
409
- /**
410
- * Delete records.
411
- *
412
- * @param {string} model - Model name
413
- * @param {number[]} ids - Record IDs to delete
414
- * @returns {Promise<boolean>}
415
- */
416
- export async function unlink(model, ids) {
417
- return await call(model, 'unlink', [ids])
418
- }
419
-
420
- /**
421
- * Get count of records matching domain.
422
- *
423
- * @param {string} model - Model name
424
- * @param {Array[]} [domain=[]] - Search domain
425
- * @returns {Promise<number>}
426
- */
427
- export async function searchCount(model, domain = []) {
428
- return await call(model, 'search_count', [domain])
429
- }
430
-
431
- /**
432
- * Get list of available databases.
433
- *
434
- * @returns {Promise<string[]>}
435
- */
436
- export async function listDatabases() {
437
- return await jsonRpc('db', 'list', [])}
438
-
439
- /**
440
- * Check version of Odoo server.
441
- *
442
- * @returns {Promise<Object>} Version info
443
- */
444
- export async function versionInfo() {
445
- const response = await fetch(`${_config.baseUrl}/web/webclient/version_info`, {
446
- method: 'POST',
447
- headers: { 'Content-Type': 'application/json' },
448
- body: '{}'
449
- })
450
-
451
- if (!response.ok) {
452
- throw new Error(`[metaowl] Failed to get version info: ${response.status}`)
453
- }
454
-
455
- const data = await response.json()
456
- return data.result
457
- }
458
-
459
- /**
460
- * Register auth state change listener.
461
- *
462
- * @param {Function} callback - Called with (session|null) when auth changes
463
- * @returns {Function} Unsubscribe function
464
- */
465
- export function onAuthChange(callback) {
466
- _authListeners.push(callback)
467
- return () => {
468
- const index = _authListeners.indexOf(callback)
469
- if (index > -1) {
470
- _authListeners.splice(index, 1)
471
- }
472
- }
473
- }
474
-
475
- /**
476
- * Notify all auth listeners.
477
- */
478
- function notifyAuthListeners() {
479
- for (const listener of _authListeners) {
480
- try {
481
- listener(_session)
482
- } catch {
483
- // Ignore listener errors
484
- }
485
- }
486
- }
487
-
488
- /**
489
- * OdooService namespace for convenient access.
490
- */
491
- export const OdooService = {
492
- configure,
493
- getConfig,
494
- isConfigured,
495
- authenticate,
496
- isAuthenticated,
497
- getSession,
498
- logout,
499
- searchRead,
500
- call,
501
- read,
502
- create,
503
- write,
504
- unlink,
505
- searchCount,
506
- listDatabases,
507
- versionInfo,
508
- onAuthChange
509
- }
510
-
511
- export default OdooService