@stonecrop/stonecrop 0.2.5 → 0.2.7

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/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "@stonecrop/stonecrop",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "schema helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "author": {
8
+ "name": "Tyler Matteson",
9
+ "email": "tyler@agritheory.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/agritheory/stonecrop",
14
+ "directory": "stonecrop"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/agritheory/stonecrop/issues"
18
+ },
7
19
  "exports": {
8
20
  ".": {
9
21
  "import": "./dist/stonecrop.js",
@@ -12,10 +24,11 @@
12
24
  },
13
25
  "main": "dist/stonecrop.js",
14
26
  "module": "dist/stonecrop.js",
27
+ "umd": "dist/stonecrop.umd.cjs",
15
28
  "types": "src/index",
16
29
  "files": [
17
30
  "dist/*",
18
- "src/**/*.vue"
31
+ "src/*"
19
32
  ],
20
33
  "dependencies": {
21
34
  "@vueuse/core": "^9.13.0",
@@ -24,26 +37,28 @@
24
37
  "pinia-shared-state": "^0.3.0",
25
38
  "pinia-undo": "^0.1.9",
26
39
  "pinia-xstate": "^1.0.9",
27
- "vue": "^3.2.47",
40
+ "vue": "^3.4.23",
28
41
  "vue-router": "^4",
29
42
  "xstate": "~4.37.2"
30
43
  },
31
44
  "devDependencies": {
32
- "@typescript-eslint/eslint-plugin": "^5.59.5",
33
- "@typescript-eslint/parser": "^5.59.5",
34
- "@vitejs/plugin-vue": "^4.2.1",
45
+ "@typescript-eslint/eslint-plugin": "^7.6.0",
46
+ "@typescript-eslint/parser": "^7.6.0",
47
+ "@vitejs/plugin-vue": "^5.0.4",
35
48
  "eslint": "^8.40.0",
36
49
  "eslint-config-prettier": "^8.8.0",
37
50
  "eslint-plugin-vue": "^9.11.1",
38
- "typescript": "^5.0.4",
39
- "vite": "^4.3.5",
40
- "@stonecrop/aform": "0.2.5",
41
- "@stonecrop/atable": "0.2.5"
51
+ "typescript": "^5.4.5",
52
+ "vite": "^5.2.9",
53
+ "@stonecrop/aform": "0.2.7",
54
+ "@stonecrop/atable": "0.2.7"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
42
58
  },
43
59
  "engines": {
44
60
  "node": ">=20.11.0"
45
61
  },
46
- "umd": "dist/stonecrop.umd.cjs",
47
62
  "scripts": {
48
63
  "build": "vite build",
49
64
  "dev": "vite serve stories/ -c vite.config.ts",
@@ -0,0 +1,49 @@
1
+ import { inject, onBeforeMount, Ref, ref } from 'vue'
2
+
3
+ import Registry from './registry'
4
+ import { Stonecrop } from './stonecrop'
5
+ import { useDataStore } from './stores/data'
6
+
7
+ type StonecropReturn = {
8
+ stonecrop: Ref<Stonecrop>
9
+ isReady: Ref<boolean>
10
+ }
11
+
12
+ export function useStonecrop(registry?: Registry): StonecropReturn {
13
+ if (!registry) {
14
+ registry = inject<Registry>('$registry')
15
+ }
16
+
17
+ const store = useDataStore()
18
+ const stonecrop = ref(new Stonecrop(registry, store))
19
+ const isReady = ref(false)
20
+
21
+ onBeforeMount(async () => {
22
+ const route = registry.router.currentRoute.value
23
+ const doctypeSlug = route.params.records?.toString().toLowerCase()
24
+ const recordId = route.params.record?.toString().toLowerCase()
25
+
26
+ // TODO: handle views other than list and form views?
27
+ if (!doctypeSlug && !recordId) {
28
+ return
29
+ }
30
+
31
+ // setup doctype via registry
32
+ const doctype = await registry.getMeta(doctypeSlug)
33
+ registry.addDoctype(doctype)
34
+ stonecrop.value.setup(doctype)
35
+
36
+ if (doctypeSlug) {
37
+ if (recordId) {
38
+ await stonecrop.value.getRecord(doctype, recordId)
39
+ } else {
40
+ await stonecrop.value.getRecords(doctype)
41
+ }
42
+ }
43
+
44
+ stonecrop.value.runAction(doctype, 'LOAD', recordId ? [recordId] : undefined)
45
+ isReady.value = true
46
+ })
47
+
48
+ return { stonecrop, isReady }
49
+ }
package/src/doctype.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { Component } from 'vue'
2
+
3
+ import type { ImmutableDoctype } from 'types/index'
4
+
5
+ export default class DoctypeMeta {
6
+ readonly doctype: string
7
+ readonly schema: ImmutableDoctype['schema']
8
+ readonly workflow: ImmutableDoctype['workflow']
9
+ readonly actions: ImmutableDoctype['actions']
10
+ // TODO: allow different components for different views; probably
11
+ // should be defined in the schema instead?
12
+ readonly component?: Component
13
+
14
+ constructor(
15
+ doctype: string,
16
+ schema: ImmutableDoctype['schema'],
17
+ workflow: ImmutableDoctype['workflow'],
18
+ actions: ImmutableDoctype['actions'],
19
+ component?: Component
20
+ ) {
21
+ this.doctype = doctype
22
+ this.schema = schema
23
+ this.workflow = workflow
24
+ this.actions = actions
25
+ this.component = component
26
+ }
27
+
28
+ get slug() {
29
+ // kebab case
30
+ return this.doctype
31
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
32
+ .replace(/[\s_]+/g, '-')
33
+ .toLowerCase()
34
+ }
35
+
36
+ get __typename() {
37
+ return this.doctype
38
+ }
39
+ }
@@ -0,0 +1,13 @@
1
+ export function NotImplementedError(message: string) {
2
+ this.message = message || ''
3
+ }
4
+
5
+ NotImplementedError.prototype = Object.create(Error.prototype, {
6
+ constructor: { value: NotImplementedError },
7
+ name: { value: 'NotImplemented' },
8
+ stack: {
9
+ get: function () {
10
+ return new Error().stack
11
+ },
12
+ },
13
+ })
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { useStonecrop } from './composable'
2
+ import DoctypeMeta from './doctype'
3
+ import Registry from './registry'
4
+ import Stonecrop from './plugins'
5
+
6
+ export { DoctypeMeta, Registry, Stonecrop, useStonecrop }
@@ -0,0 +1,23 @@
1
+ import { App } from 'vue'
2
+
3
+ import type { InstallOptions } from 'types/index'
4
+ import Registry from '../registry'
5
+ import router from '../router'
6
+ import { pinia } from '../stores'
7
+
8
+ export default {
9
+ install: (app: App, options?: InstallOptions) => {
10
+ const appRouter = options?.router || router
11
+ const registry = new Registry(appRouter, options?.getMeta)
12
+
13
+ app.use(appRouter)
14
+ app.use(pinia)
15
+ app.provide('$registry', registry)
16
+
17
+ if (options?.components) {
18
+ for (const [tag, component] of Object.entries(options.components)) {
19
+ app.component(tag, component)
20
+ }
21
+ }
22
+ },
23
+ }
@@ -0,0 +1,35 @@
1
+ import { Router } from 'vue-router'
2
+
3
+ import DoctypeMeta from '@/doctype'
4
+
5
+ export default class Registry {
6
+ static _root: Registry
7
+ name: string
8
+ router: Router
9
+ registry: Record<string, DoctypeMeta>
10
+ getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>
11
+
12
+ constructor(router: Router, getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>) {
13
+ if (Registry._root) {
14
+ return Registry._root
15
+ }
16
+ Registry._root = this
17
+ this.name = 'Registry'
18
+ this.router = router
19
+ this.registry = {}
20
+ this.getMeta = getMeta
21
+ }
22
+
23
+ addDoctype(doctype: DoctypeMeta) {
24
+ if (!(doctype.doctype in Object.keys(this.registry))) {
25
+ this.registry[doctype.slug] = doctype
26
+ }
27
+ if (!this.router.hasRoute(doctype.doctype)) {
28
+ this.router.addRoute({
29
+ path: `/${doctype.slug}`,
30
+ name: doctype.slug,
31
+ component: doctype.component,
32
+ })
33
+ }
34
+ }
35
+ }
package/src/router.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+
3
+ const router = createRouter({
4
+ history: createWebHistory(),
5
+ routes: [],
6
+ })
7
+
8
+ export default router
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import { ComponentOptions } from 'vue'
3
+ const Component: ComponentOptions
4
+ export default Component
5
+ }
@@ -0,0 +1,243 @@
1
+ import type { ImmutableDoctype, Schema } from 'types/index'
2
+ import DoctypeMeta from './doctype'
3
+ import { NotImplementedError } from './exceptions'
4
+ import Registry from './registry'
5
+ import { useDataStore } from './stores/data'
6
+
7
+ export class Stonecrop {
8
+ /**
9
+ * @property {Stonecrop} _root
10
+ * @description The root Stonecrop instance
11
+ */
12
+ static _root: Stonecrop
13
+
14
+ /**
15
+ * @property {string} name
16
+ * @description The name of the Stonecrop instance
17
+ * @example
18
+ * 'Stonecrop'
19
+ */
20
+ readonly name = 'Stonecrop'
21
+
22
+ /**
23
+ * @property {Registry} registry
24
+ * @description The registry is an immutable collection of doctypes
25
+ * @example
26
+ * {
27
+ * 'task': {
28
+ * doctype: 'Task',
29
+ * schema: {
30
+ * title: 'string',
31
+ * description: 'string',
32
+ * ...
33
+ * }
34
+ * },
35
+ * ...
36
+ * }
37
+ * @see {@link Registry}
38
+ * @see {@link DoctypeMeta}
39
+ */
40
+ readonly registry: Registry
41
+
42
+ /**
43
+ * @property {Schema} schema - The Stonecrop schema
44
+ * @description The schema is a subset of the registry
45
+ * @example
46
+ * {
47
+ * doctype: 'Task',
48
+ * schema: {
49
+ * title: 'string',
50
+ * description: 'string',
51
+ * ...
52
+ * }
53
+ * }
54
+ * @see {@link Registry}
55
+ * @see {@link DoctypeMeta}
56
+ * @see {@link DoctypeMeta.schema}
57
+ */
58
+ schema: Schema
59
+
60
+ /**
61
+ * @property {ImmutableDoctype['workflow']} workflow
62
+ * @description The workflow is a subset of the registry
63
+ */
64
+ workflow: ImmutableDoctype['workflow']
65
+
66
+ /**
67
+ * @property {ImmutableDoctype['actions']} actions
68
+ * @description The actions are a subset of the registry
69
+ */
70
+ actions: ImmutableDoctype['actions']
71
+
72
+ /**
73
+ * @property {ReturnType<typeof useDataStore>} store
74
+ * @description The Pinia store that manages the mutable records
75
+ */
76
+ store: ReturnType<typeof useDataStore>
77
+
78
+ /**
79
+ * @constructor
80
+ * @param {Registry} registry - The immutable registry
81
+ * @param {ReturnType<typeof useDataStore>} store - The mutable Pinia store
82
+ * @param {Schema} [schema] - (optional) The Stonecrop schema
83
+ * @param {ImmutableDoctype['workflow']} [workflow] - (optional) The Stonecrop workflow
84
+ * @param {ImmutableDoctype['actions']} [actions] - (optional) The Stonecrop actions
85
+ * @returns {Stonecrop} The Stonecrop instance
86
+ * @description The Stonecrop constructor initializes a new Stonecrop instance with the given registry, store, schema, workflow, and actions. If a Stonecrop instance has already been created, it returns the existing instance instead of creating a new one.
87
+ * @example
88
+ * const registry = new Registry()
89
+ * const store = useDataStore()
90
+ * const stonecrop = new Stonecrop(registry, store, schema, workflow, actions)
91
+ */
92
+ constructor(
93
+ registry: Registry,
94
+ store: ReturnType<typeof useDataStore>,
95
+ schema?: Schema,
96
+ workflow?: ImmutableDoctype['workflow'],
97
+ actions?: ImmutableDoctype['actions']
98
+ ) {
99
+ if (Stonecrop._root) {
100
+ return Stonecrop._root
101
+ }
102
+ Stonecrop._root = this
103
+ this.registry = registry
104
+ this.store = store
105
+ this.schema = schema // new Registry(schema)
106
+ this.workflow = workflow
107
+ this.actions = actions
108
+ }
109
+
110
+ /**
111
+ * @method setup
112
+ * @param {DoctypeMeta} doctype - The doctype to setup
113
+ * @returns {void}
114
+ * @description Sets up the Stonecrop instance with the given doctype
115
+ * @example
116
+ * const doctype = await registry.getMeta('Task')
117
+ * stonecrop.setup(doctype)
118
+ */
119
+ setup(doctype: DoctypeMeta): void {
120
+ this.getMeta(doctype)
121
+ this.getWorkflow(doctype)
122
+ this.getActions(doctype)
123
+ }
124
+
125
+ /**
126
+ * @method getMeta
127
+ * @param {DoctypeMeta} doctype - The doctype to get meta for
128
+ * @returns {DoctypeMeta}
129
+ * @see {@link DoctypeMeta}
130
+ * @throws NotImplementedError
131
+ * @description Gets the meta for the given doctype
132
+ * @example
133
+ * const doctype = await registry.getMeta('Task')
134
+ * const meta = stonecrop.getMeta(doctype)
135
+ */
136
+ getMeta(doctype: DoctypeMeta): DoctypeMeta | Promise<DoctypeMeta> | never {
137
+ return this.registry.getMeta ? this.registry.getMeta(doctype.doctype) : new NotImplementedError(doctype.doctype)
138
+ }
139
+
140
+ /**
141
+ * @method getWorkflow
142
+ * @param {DoctypeMeta} doctype - The doctype to get workflow for
143
+ * @returns {void}
144
+ * @description Gets the workflow for the given doctype
145
+ * @example
146
+ * const doctype = await registry.getMeta('Task')
147
+ * stonecrop.getWorkflow(doctype)
148
+ */
149
+ getWorkflow(doctype: DoctypeMeta): void {
150
+ const doctypeRegistry = this.registry.registry[doctype.slug]
151
+ this.workflow = doctypeRegistry.workflow
152
+ }
153
+
154
+ /**
155
+ * @method getActions
156
+ * @param {DoctypeMeta} doctype - The doctype to get actions for
157
+ * @returns {void}
158
+ * @description Gets the actions for the given doctype
159
+ * @example
160
+ * const doctype = await registry.getMeta('Task')
161
+ * stonecrop.getActions(doctype)
162
+ */
163
+ getActions(doctype: DoctypeMeta): void {
164
+ const doctypeRegistry = this.registry.registry[doctype.slug]
165
+ this.actions = doctypeRegistry.actions
166
+ }
167
+
168
+ /**
169
+ * @method getRecords
170
+ * @param {DoctypeMeta} doctype - The doctype to get records for
171
+ * @param {RequestInit} [filters] - The filters to apply to the records
172
+ * @returns {Promise<void>}
173
+ * @description Gets the records for the given doctype
174
+ * @example
175
+ * const doctype = await registry.getMeta('Task')
176
+ * await stonecrop.getRecords(doctype)
177
+ * @example
178
+ * const doctype = await registry.getMeta('Task')
179
+ * const filters = JSON.stringify({ status: 'Open' })
180
+ * await stonecrop.getRecords(doctype, { body: filters })
181
+ */
182
+ async getRecords(doctype: DoctypeMeta, filters?: RequestInit): Promise<void> {
183
+ this.store.$patch({ records: [] })
184
+ const records = await fetch(`/${doctype.slug}`, filters)
185
+ const data: Record<string, any>[] = await records.json()
186
+ this.store.$patch({ records: data })
187
+ }
188
+
189
+ /**
190
+ * @method getRecord
191
+ * @param {DoctypeMeta} doctype - The doctype to get record for
192
+ * @param {string} id - The id of the record to get
193
+ * @returns {Promise<void>}
194
+ * @description Gets the record for the given doctype and id
195
+ * @example
196
+ * const doctype = await registry.getMeta('Task')
197
+ * await stonecrop.getRecord(doctype, 'TASK-00001')
198
+ */
199
+ async getRecord(doctype: DoctypeMeta, id: string): Promise<void> {
200
+ this.store.$patch({ record: {} })
201
+ const record = await fetch(`/${doctype.slug}/${id}`)
202
+ const data: Record<string, any> = await record.json()
203
+ this.store.$patch({ record: data })
204
+ }
205
+
206
+ /**
207
+ * @method runAction
208
+ * @param {DoctypeMeta} doctype - The doctype to run action for
209
+ * @param {string} action - The action to run
210
+ * @param {string[]} [id] - The id(s) of the record(s) to run action on
211
+ * @returns {void}
212
+ * @description Runs the action for the given doctype and id
213
+ * @example
214
+ * const doctype = await registry.getMeta('Task')
215
+ * stonecrop.runAction(doctype, 'CREATE')
216
+ * @example
217
+ * const doctype = await registry.getMeta('Task')
218
+ * stonecrop.runAction(doctype, 'UPDATE', ['TASK-00001'])
219
+ * @example
220
+ * const doctype = await registry.getMeta('Task')
221
+ * stonecrop.runAction(doctype, 'DELETE', ['TASK-00001'])
222
+ * @example
223
+ * const doctype = await registry.getMeta('Task')
224
+ * stonecrop.runAction(doctype, 'TRANSITION', ['TASK-00001', 'TASK-00002'])
225
+ */
226
+ runAction(doctype: DoctypeMeta, action: string, id?: string[]): void {
227
+ const doctypeRegistry = this.registry.registry[doctype.slug]
228
+ const actions = doctypeRegistry.actions.get(action)
229
+
230
+ // trigger the action on the state machine
231
+ const { initialState } = this.workflow
232
+ this.workflow.transition(initialState, { type: action })
233
+
234
+ // run actions after state machine transition
235
+ if (actions.length > 0) {
236
+ actions.forEach(action => {
237
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
238
+ const actionFn = new Function(action)
239
+ actionFn(id)
240
+ })
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,8 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+
4
+ export const useDataStore = defineStore('data', () => {
5
+ const records = ref<Record<string, any>[]>([])
6
+ const record = ref<Record<string, any>>({})
7
+ return { records, record }
8
+ })
@@ -0,0 +1,16 @@
1
+ import { createPinia } from 'pinia'
2
+ import { PiniaSharedState } from 'pinia-shared-state'
3
+ import { PiniaUndo } from 'pinia-undo'
4
+
5
+ const pinia = createPinia()
6
+
7
+ // Pass the plugin to your application's pinia plugin
8
+ pinia.use(
9
+ PiniaSharedState({
10
+ enable: true,
11
+ initialize: true,
12
+ })
13
+ )
14
+ // pinia.use(PiniaUndo)
15
+
16
+ export { pinia }
@@ -0,0 +1,35 @@
1
+ import { defineStore } from 'pinia'
2
+ import xstate from 'pinia-xstate'
3
+ import { createMachine } from 'xstate'
4
+
5
+ export const counterMachine = createMachine(
6
+ {
7
+ id: 'counter',
8
+ initial: 'active',
9
+ context: {
10
+ count: 0,
11
+ },
12
+ tsTypes: {} as import('./xstate.typegen').Typegen0,
13
+ states: {
14
+ active: {
15
+ on: {
16
+ INC: { actions: 'increment' },
17
+ DEC: { actions: 'decrement' },
18
+ },
19
+ },
20
+ },
21
+ },
22
+ {
23
+ actions: {
24
+ increment: context => {
25
+ context.count = context.count + 1
26
+ },
27
+ decrement: context => {
28
+ context.count = context.count - 1
29
+ },
30
+ },
31
+ }
32
+ )
33
+
34
+ // create a store using the xstate middleware
35
+ export const useCounterStore = defineStore(counterMachine.id, xstate(counterMachine))