teamplay 0.0.1
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/README.md +95 -0
- package/connect/index.js +12 -0
- package/connect/sharedbConnection.cjs +3 -0
- package/connect/test.js +12 -0
- package/index.js +44 -0
- package/orm/$.js +38 -0
- package/orm/Cache.js +29 -0
- package/orm/Doc.js +232 -0
- package/orm/Query.js +278 -0
- package/orm/Reaction.js +44 -0
- package/orm/Root.js +41 -0
- package/orm/Signal.js +211 -0
- package/orm/Value.js +27 -0
- package/orm/addModel.js +31 -0
- package/orm/connection.js +26 -0
- package/orm/dataTree.js +196 -0
- package/orm/getSignal.js +100 -0
- package/orm/sub.js +32 -0
- package/package.json +60 -0
- package/react/convertToObserver.js +93 -0
- package/react/executionContextTracker.js +32 -0
- package/react/helpers.js +35 -0
- package/react/observer.js +9 -0
- package/react/trapRender.js +45 -0
- package/react/universal$.js +9 -0
- package/react/universalSub.js +9 -0
- package/react/wrapIntoSuspense.js +63 -0
- package/schema/GUID_PATTERN.js +1 -0
- package/schema/associations.js +51 -0
- package/schema/pickFormFields.js +104 -0
- package/server.js +12 -0
- package/utils/uuid.cjs +2 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useMemo, forwardRef as _forwardRef, memo, createElement as el, Suspense } from 'react'
|
|
2
|
+
import { createCaches } from '@startupjs/cache'
|
|
3
|
+
import { __increment, __decrement } from '@startupjs/debug'
|
|
4
|
+
import { pipeComponentMeta, pipeComponentDisplayName, ComponentMetaContext, useUnmount } from './helpers.js'
|
|
5
|
+
|
|
6
|
+
export default function wrapIntoSuspense (
|
|
7
|
+
ObservedComponent
|
|
8
|
+
) {
|
|
9
|
+
const { forwardRef, suspenseProps } = ObservedComponent.__observerOptions
|
|
10
|
+
if (!(suspenseProps && suspenseProps.fallback)) {
|
|
11
|
+
throw Error(
|
|
12
|
+
'[observer()] You must pass at least ' +
|
|
13
|
+
'a fallback parameter to suspenseProps'
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let SuspenseWrapper = (props, ref) => {
|
|
18
|
+
const cache = useMemo(() => {
|
|
19
|
+
__increment('ObserverWrapper.cache')
|
|
20
|
+
return createCaches(['styles', 'model'])
|
|
21
|
+
}, [])
|
|
22
|
+
// TODO: using useState instead of useMemo will keep this intact during Fast Refresh
|
|
23
|
+
// Research if we can change it to use it.
|
|
24
|
+
// const [componentMeta] = React.useState({
|
|
25
|
+
// componentId: $root.id(),
|
|
26
|
+
// createdAt: Date.now(),
|
|
27
|
+
// cache
|
|
28
|
+
// })
|
|
29
|
+
const componentMeta = useMemo(function () {
|
|
30
|
+
return {
|
|
31
|
+
// componentId: $root.id(), // TODO: implement creating a unique component guid here (if it's needed anymore)
|
|
32
|
+
createdAt: Date.now(),
|
|
33
|
+
cache
|
|
34
|
+
}
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
useUnmount(() => {
|
|
38
|
+
__decrement('ObserverWrapper.cache')
|
|
39
|
+
cache.clear()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (forwardRef) props = { ...props, ref }
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
el(ComponentMetaContext.Provider, { value: componentMeta },
|
|
46
|
+
el(Suspense, suspenseProps,
|
|
47
|
+
el(ObservedComponent, props)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// pipe only displayName because forwardRef render function
|
|
54
|
+
// do not support propTypes or defaultProps
|
|
55
|
+
pipeComponentDisplayName(ObservedComponent, SuspenseWrapper, 'StartupjsObserverWrapper')
|
|
56
|
+
|
|
57
|
+
if (forwardRef) SuspenseWrapper = _forwardRef(SuspenseWrapper)
|
|
58
|
+
SuspenseWrapper = memo(SuspenseWrapper)
|
|
59
|
+
|
|
60
|
+
pipeComponentMeta(ObservedComponent, SuspenseWrapper)
|
|
61
|
+
|
|
62
|
+
return SuspenseWrapper
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import GUID_PATTERN from './GUID_PATTERN.js'
|
|
2
|
+
|
|
3
|
+
export function belongsTo (collectionName) {
|
|
4
|
+
return {
|
|
5
|
+
type: 'string',
|
|
6
|
+
pattern: GUID_PATTERN,
|
|
7
|
+
$association: {
|
|
8
|
+
type: 'belongsTo',
|
|
9
|
+
collection: collectionName
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasMany (collectionName) {
|
|
15
|
+
return {
|
|
16
|
+
type: 'array',
|
|
17
|
+
items: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
pattern: GUID_PATTERN
|
|
20
|
+
},
|
|
21
|
+
$association: {
|
|
22
|
+
type: 'hasMany',
|
|
23
|
+
collection: collectionName
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasManyFlags (collectionName) {
|
|
29
|
+
return {
|
|
30
|
+
type: 'object',
|
|
31
|
+
patternProperties: {
|
|
32
|
+
[GUID_PATTERN]: { type: 'boolean' }
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
$association: {
|
|
36
|
+
type: 'hasManyFlags',
|
|
37
|
+
collection: collectionName
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasOne (collectionName) {
|
|
43
|
+
return {
|
|
44
|
+
type: 'string',
|
|
45
|
+
pattern: GUID_PATTERN,
|
|
46
|
+
$association: {
|
|
47
|
+
type: 'hasOne',
|
|
48
|
+
collection: collectionName
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick properties from json-schema to be used in a form.
|
|
3
|
+
* Supports simplified schema (just the properties object) and full schema.
|
|
4
|
+
* Performs extra transformations like auto-generating `label`.
|
|
5
|
+
* `createdAt`, `updatedAt`, `id`, `_id` fields are excluded by default.
|
|
6
|
+
* @param {Object} schema
|
|
7
|
+
* @param {Object|Array} options - exclude or include fields. If array, it's the same as passing { include: [...] }
|
|
8
|
+
* @param {Array} options.include - list of fields to pick (default: all)
|
|
9
|
+
* @param {Array} options.exclude - list of fields to exclude (default: none)
|
|
10
|
+
* @param {Boolean} options.freeze - whether to deep freeze the result (default: true)
|
|
11
|
+
*/
|
|
12
|
+
export default function pickFormFields (schema, options) {
|
|
13
|
+
try {
|
|
14
|
+
let include, exclude, freeze
|
|
15
|
+
if (Array.isArray(options)) {
|
|
16
|
+
include = options
|
|
17
|
+
} else {
|
|
18
|
+
;({ include, exclude, freeze = true } = options || {})
|
|
19
|
+
}
|
|
20
|
+
exclude ??= []
|
|
21
|
+
if (!schema) throw Error('pickFormFields: schema is required')
|
|
22
|
+
schema = JSON.parse(JSON.stringify(schema))
|
|
23
|
+
if (schema.type === 'object') {
|
|
24
|
+
schema = schema.properties
|
|
25
|
+
}
|
|
26
|
+
for (const key in schema) {
|
|
27
|
+
if (shouldIncludeField(key, schema[key], { include, exclude })) {
|
|
28
|
+
const field = schema[key]
|
|
29
|
+
if (!field.label) field.label = camelCaseToLabel(key)
|
|
30
|
+
} else {
|
|
31
|
+
delete schema[key]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (freeze) return new Proxy(schema, deepFreezeHandler)
|
|
35
|
+
return schema
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw Error(`
|
|
38
|
+
pickFormFields: ${err.message}
|
|
39
|
+
schema:\n${JSON.stringify(schema, null, 2)}
|
|
40
|
+
`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Proxy handlers to deep freeze schema to prevent accidental mutations.
|
|
45
|
+
// For this, when we .get() a property, we also return the same recursive Proxy handler if it's an object.
|
|
46
|
+
const deepFreezeHandler = {
|
|
47
|
+
get (target, prop) {
|
|
48
|
+
const value = target[prop]
|
|
49
|
+
if (typeof value === 'object' && value !== null) {
|
|
50
|
+
return new Proxy(value, deepFreezeHandler)
|
|
51
|
+
}
|
|
52
|
+
return value
|
|
53
|
+
},
|
|
54
|
+
set () {
|
|
55
|
+
throw Error(ERRORS.schemaIsFrozen)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shouldIncludeField (key, field, { include, exclude = [] } = {}) {
|
|
60
|
+
if (!field) throw Error(`field "${key}" does not have a schema definition`)
|
|
61
|
+
if (include?.includes(key)) return true
|
|
62
|
+
if (exclude.includes(key)) return false
|
|
63
|
+
// if field has 'input' specified then it's an explicit indicator that it can be used in forms,
|
|
64
|
+
// so the default exclusion rules don't apply
|
|
65
|
+
if (!field.input) {
|
|
66
|
+
// exclude some meta fields by default
|
|
67
|
+
if (DEFAULT_EXCLUDE_FORM_FIELDS.includes(key)) return false
|
|
68
|
+
// exclude foreign keys by default
|
|
69
|
+
// Foreign keys have a custom `$association` property set by belongsTo/hasMany/hasOne helpers
|
|
70
|
+
if (field.$association) return false
|
|
71
|
+
}
|
|
72
|
+
// if include array is not explicitly set, include all fields by default
|
|
73
|
+
if (!include) return true
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DEFAULT_EXCLUDE_FORM_FIELDS = ['id', '_id', 'createdAt', 'updatedAt']
|
|
78
|
+
|
|
79
|
+
// split into words, capitalize first word, make others lowercase
|
|
80
|
+
function camelCaseToLabel (str) {
|
|
81
|
+
return str
|
|
82
|
+
.replace(/([A-Z])/g, ' $1')
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/^./, (s) => s.toUpperCase())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ERRORS = {
|
|
88
|
+
schemaIsFrozen: `
|
|
89
|
+
Form fields are immutable.
|
|
90
|
+
If you want to change them, clone them with \`JSON.parse(JSON.stringify(FORM_FIELDS))\`.
|
|
91
|
+
|
|
92
|
+
If you want to do it inside react component, you can use this pattern for the most effective cloning:
|
|
93
|
+
|
|
94
|
+
\`\`\`
|
|
95
|
+
const $fields = useValue$(useMemo(() => JSON.parse(JSON.stringify(FORM_FIELDS)), []))
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
and then pass $fields to the Form component like this:
|
|
99
|
+
|
|
100
|
+
\`\`\`
|
|
101
|
+
<Form $fields={$fields} $value={$value} />
|
|
102
|
+
\`\`\`
|
|
103
|
+
`
|
|
104
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import createChannel from '@startupjs/channel/server'
|
|
2
|
+
import { connection, setConnection, setFetchOnly } from './orm/connection.js'
|
|
3
|
+
|
|
4
|
+
export { default as ShareDB } from 'sharedb'
|
|
5
|
+
|
|
6
|
+
export default function initConnection (backend, options) {
|
|
7
|
+
if (!backend) throw Error('backend is required')
|
|
8
|
+
if (connection) throw Error('Connection already exists')
|
|
9
|
+
setConnection(backend.connect())
|
|
10
|
+
setFetchOnly(true)
|
|
11
|
+
return createChannel(backend, options)
|
|
12
|
+
}
|
package/utils/uuid.cjs
ADDED