@jskit-ai/crud 0.1.4
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.descriptor.mjs +322 -0
- package/package.json +22 -0
- package/src/client/index.js +3 -0
- package/src/server/CrudServiceProvider.js +11 -0
- package/src/server/actionIds.js +22 -0
- package/src/server/actions.js +152 -0
- package/src/server/registerRoutes.js +235 -0
- package/src/server/repository.js +162 -0
- package/src/server/service.js +96 -0
- package/src/shared/crud/crudModuleConfig.js +310 -0
- package/src/shared/crud/crudResource.js +191 -0
- package/src/shared/index.js +12 -0
- package/templates/migrations/crud_initial.cjs +42 -0
- package/templates/src/elements/CreateElement.vue +115 -0
- package/templates/src/elements/EditElement.vue +140 -0
- package/templates/src/elements/ListElement.vue +88 -0
- package/templates/src/elements/ViewElement.vue +126 -0
- package/templates/src/elements/clientSupport.js +41 -0
- package/templates/src/local-package/client/index.js +4 -0
- package/templates/src/local-package/package.descriptor.mjs +83 -0
- package/templates/src/local-package/package.json +14 -0
- package/templates/src/local-package/server/CrudServiceProvider.js +87 -0
- package/templates/src/local-package/server/actionIds.js +9 -0
- package/templates/src/local-package/server/actions.js +151 -0
- package/templates/src/local-package/server/diTokens.js +4 -0
- package/templates/src/local-package/server/registerRoutes.js +196 -0
- package/templates/src/local-package/server/repository.js +1 -0
- package/templates/src/local-package/server/service.js +96 -0
- package/templates/src/local-package/shared/crudResource.js +1 -0
- package/templates/src/local-package/shared/index.js +8 -0
- package/templates/src/local-package/shared/moduleConfig.js +169 -0
- package/templates/src/pages/admin/crud/[recordId]/edit.vue +7 -0
- package/templates/src/pages/admin/crud/[recordId]/index.vue +7 -0
- package/templates/src/pages/admin/crud/index.vue +7 -0
- package/templates/src/pages/admin/crud/new.vue +7 -0
- package/test/crudModuleConfig.test.js +225 -0
- package/test/crudResource.test.js +41 -0
- package/test/crudServerGuards.test.js +61 -0
- package/test/crudService.test.js +83 -0
- package/test/routeInputContracts.test.js +211 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cursorPaginationQueryValidator,
|
|
3
|
+
recordIdParamsValidator
|
|
4
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
5
|
+
import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
6
|
+
import { crudResource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
7
|
+
import { actionIds } from "./actionIds.js";
|
|
8
|
+
|
|
9
|
+
function requireActionSurface(surface = "") {
|
|
10
|
+
const normalizedSurface = String(surface || "").trim().toLowerCase();
|
|
11
|
+
if (!normalizedSurface) {
|
|
12
|
+
throw new TypeError("createActions requires a non-empty surface.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return normalizedSurface;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createActions({ surface = "" } = {}) {
|
|
19
|
+
const actionSurface = requireActionSurface(surface);
|
|
20
|
+
|
|
21
|
+
return Object.freeze([
|
|
22
|
+
{
|
|
23
|
+
id: actionIds.list,
|
|
24
|
+
version: 1,
|
|
25
|
+
kind: "query",
|
|
26
|
+
channels: ["api", "automation", "internal"],
|
|
27
|
+
surfaces: [actionSurface],
|
|
28
|
+
permission: {
|
|
29
|
+
require: "authenticated"
|
|
30
|
+
},
|
|
31
|
+
inputValidator: [workspaceSlugParamsValidator, cursorPaginationQueryValidator],
|
|
32
|
+
outputValidator: crudResource.operations.list.outputValidator,
|
|
33
|
+
idempotency: "none",
|
|
34
|
+
audit: {
|
|
35
|
+
actionName: actionIds.list
|
|
36
|
+
},
|
|
37
|
+
observability: {},
|
|
38
|
+
async execute(input, context, deps) {
|
|
39
|
+
return deps.${option:namespace|camel}Service.listRecords(input, {
|
|
40
|
+
context,
|
|
41
|
+
visibilityContext: context?.visibilityContext
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: actionIds.view,
|
|
47
|
+
version: 1,
|
|
48
|
+
kind: "query",
|
|
49
|
+
channels: ["api", "automation", "internal"],
|
|
50
|
+
surfaces: [actionSurface],
|
|
51
|
+
permission: {
|
|
52
|
+
require: "authenticated"
|
|
53
|
+
},
|
|
54
|
+
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
|
|
55
|
+
outputValidator: crudResource.operations.view.outputValidator,
|
|
56
|
+
idempotency: "none",
|
|
57
|
+
audit: {
|
|
58
|
+
actionName: actionIds.view
|
|
59
|
+
},
|
|
60
|
+
observability: {},
|
|
61
|
+
async execute(input, context, deps) {
|
|
62
|
+
return deps.${option:namespace|camel}Service.getRecord(input.recordId, {
|
|
63
|
+
context,
|
|
64
|
+
visibilityContext: context?.visibilityContext
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: actionIds.create,
|
|
70
|
+
version: 1,
|
|
71
|
+
kind: "command",
|
|
72
|
+
channels: ["api", "automation", "internal"],
|
|
73
|
+
surfaces: [actionSurface],
|
|
74
|
+
permission: {
|
|
75
|
+
require: "authenticated"
|
|
76
|
+
},
|
|
77
|
+
inputValidator: [
|
|
78
|
+
workspaceSlugParamsValidator,
|
|
79
|
+
{
|
|
80
|
+
payload: crudResource.operations.create.bodyValidator
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
outputValidator: crudResource.operations.create.outputValidator,
|
|
84
|
+
idempotency: "optional",
|
|
85
|
+
audit: {
|
|
86
|
+
actionName: actionIds.create
|
|
87
|
+
},
|
|
88
|
+
observability: {},
|
|
89
|
+
async execute(input, context, deps) {
|
|
90
|
+
return deps.${option:namespace|camel}Service.createRecord(input.payload, {
|
|
91
|
+
context,
|
|
92
|
+
visibilityContext: context?.visibilityContext
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: actionIds.update,
|
|
98
|
+
version: 1,
|
|
99
|
+
kind: "command",
|
|
100
|
+
channels: ["api", "automation", "internal"],
|
|
101
|
+
surfaces: [actionSurface],
|
|
102
|
+
permission: {
|
|
103
|
+
require: "authenticated"
|
|
104
|
+
},
|
|
105
|
+
inputValidator: [
|
|
106
|
+
workspaceSlugParamsValidator,
|
|
107
|
+
recordIdParamsValidator,
|
|
108
|
+
{
|
|
109
|
+
patch: crudResource.operations.patch.bodyValidator
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
outputValidator: crudResource.operations.patch.outputValidator,
|
|
113
|
+
idempotency: "optional",
|
|
114
|
+
audit: {
|
|
115
|
+
actionName: actionIds.update
|
|
116
|
+
},
|
|
117
|
+
observability: {},
|
|
118
|
+
async execute(input, context, deps) {
|
|
119
|
+
return deps.${option:namespace|camel}Service.updateRecord(input.recordId, input.patch, {
|
|
120
|
+
context,
|
|
121
|
+
visibilityContext: context?.visibilityContext
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: actionIds.delete,
|
|
127
|
+
version: 1,
|
|
128
|
+
kind: "command",
|
|
129
|
+
channels: ["api", "automation", "internal"],
|
|
130
|
+
surfaces: [actionSurface],
|
|
131
|
+
permission: {
|
|
132
|
+
require: "authenticated"
|
|
133
|
+
},
|
|
134
|
+
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
|
|
135
|
+
outputValidator: crudResource.operations.delete.outputValidator,
|
|
136
|
+
idempotency: "optional",
|
|
137
|
+
audit: {
|
|
138
|
+
actionName: actionIds.delete
|
|
139
|
+
},
|
|
140
|
+
observability: {},
|
|
141
|
+
async execute(input, context, deps) {
|
|
142
|
+
return deps.${option:namespace|camel}Service.deleteRecord(input.recordId, {
|
|
143
|
+
context,
|
|
144
|
+
visibilityContext: context?.visibilityContext
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { createActions };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN = "crud.${option:namespace|snake}";
|
|
2
|
+
const NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN = "repository.${option:namespace|snake}";
|
|
3
|
+
|
|
4
|
+
export { NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN, NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import {
|
|
5
|
+
cursorPaginationQueryValidator,
|
|
6
|
+
recordIdParamsValidator
|
|
7
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
8
|
+
import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
9
|
+
import { normalizeScopedRouteVisibility } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
10
|
+
import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/support/workspaceRouteInput";
|
|
11
|
+
import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
|
|
12
|
+
import { actionIds } from "./actionIds.js";
|
|
13
|
+
import { crudResource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
14
|
+
import { crudModuleConfig } from "../shared/moduleConfig.js";
|
|
15
|
+
|
|
16
|
+
function registerRoutes(
|
|
17
|
+
app,
|
|
18
|
+
{
|
|
19
|
+
routeOwnershipFilter = "public",
|
|
20
|
+
routeSurface = "",
|
|
21
|
+
routeSurfaceRequiresWorkspace = false,
|
|
22
|
+
routeRelativePath = crudModuleConfig.relativePath
|
|
23
|
+
} = {}
|
|
24
|
+
) {
|
|
25
|
+
if (!app || typeof app.make !== "function") {
|
|
26
|
+
throw new Error("registerRoutes requires application make().");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
30
|
+
const routeVisibility = normalizeScopedRouteVisibility(routeOwnershipFilter, {
|
|
31
|
+
fallback: "public"
|
|
32
|
+
});
|
|
33
|
+
const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
|
|
34
|
+
const routeBase = resolveApiBasePath({
|
|
35
|
+
surfaceRequiresWorkspace: routeSurfaceRequiresWorkspace === true,
|
|
36
|
+
relativePath: routeRelativePath
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.register(
|
|
40
|
+
"GET",
|
|
41
|
+
routeBase,
|
|
42
|
+
{
|
|
43
|
+
auth: "required",
|
|
44
|
+
surface: normalizedRouteSurface,
|
|
45
|
+
visibility: routeVisibility,
|
|
46
|
+
meta: {
|
|
47
|
+
tags: ["crud"],
|
|
48
|
+
summary: "List records."
|
|
49
|
+
},
|
|
50
|
+
paramsValidator: routeParamsValidator,
|
|
51
|
+
queryValidator: cursorPaginationQueryValidator,
|
|
52
|
+
responseValidators: withStandardErrorResponses({
|
|
53
|
+
200: crudResource.operations.list.outputValidator
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
async function (request, reply) {
|
|
57
|
+
const listInput = {
|
|
58
|
+
...buildWorkspaceInputFromRouteParams(request.input.params)
|
|
59
|
+
};
|
|
60
|
+
if (request.input.query.cursor != null) {
|
|
61
|
+
listInput.cursor = request.input.query.cursor;
|
|
62
|
+
}
|
|
63
|
+
if (request.input.query.limit != null) {
|
|
64
|
+
listInput.limit = request.input.query.limit;
|
|
65
|
+
}
|
|
66
|
+
const response = await request.executeAction({
|
|
67
|
+
actionId: actionIds.list,
|
|
68
|
+
input: listInput
|
|
69
|
+
});
|
|
70
|
+
reply.code(200).send(response);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
router.register(
|
|
75
|
+
"GET",
|
|
76
|
+
`${routeBase}/:recordId`,
|
|
77
|
+
{
|
|
78
|
+
auth: "required",
|
|
79
|
+
surface: normalizedRouteSurface,
|
|
80
|
+
visibility: routeVisibility,
|
|
81
|
+
meta: {
|
|
82
|
+
tags: ["crud"],
|
|
83
|
+
summary: "View a record."
|
|
84
|
+
},
|
|
85
|
+
paramsValidator: [routeParamsValidator, recordIdParamsValidator],
|
|
86
|
+
responseValidators: withStandardErrorResponses({
|
|
87
|
+
200: crudResource.operations.view.outputValidator
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
async function (request, reply) {
|
|
91
|
+
const response = await request.executeAction({
|
|
92
|
+
actionId: actionIds.view,
|
|
93
|
+
input: {
|
|
94
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
95
|
+
recordId: request.input.params.recordId
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
reply.code(200).send(response);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
router.register(
|
|
103
|
+
"POST",
|
|
104
|
+
routeBase,
|
|
105
|
+
{
|
|
106
|
+
auth: "required",
|
|
107
|
+
surface: normalizedRouteSurface,
|
|
108
|
+
visibility: routeVisibility,
|
|
109
|
+
meta: {
|
|
110
|
+
tags: ["crud"],
|
|
111
|
+
summary: "Create a record."
|
|
112
|
+
},
|
|
113
|
+
paramsValidator: routeParamsValidator,
|
|
114
|
+
bodyValidator: crudResource.operations.create.bodyValidator,
|
|
115
|
+
responseValidators: withStandardErrorResponses(
|
|
116
|
+
{
|
|
117
|
+
201: crudResource.operations.create.outputValidator
|
|
118
|
+
},
|
|
119
|
+
{ includeValidation400: true }
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
async function (request, reply) {
|
|
123
|
+
const response = await request.executeAction({
|
|
124
|
+
actionId: actionIds.create,
|
|
125
|
+
input: {
|
|
126
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
127
|
+
payload: request.input.body
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
reply.code(201).send(response);
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
router.register(
|
|
135
|
+
"PATCH",
|
|
136
|
+
`${routeBase}/:recordId`,
|
|
137
|
+
{
|
|
138
|
+
auth: "required",
|
|
139
|
+
surface: normalizedRouteSurface,
|
|
140
|
+
visibility: routeVisibility,
|
|
141
|
+
meta: {
|
|
142
|
+
tags: ["crud"],
|
|
143
|
+
summary: "Update a record."
|
|
144
|
+
},
|
|
145
|
+
paramsValidator: [routeParamsValidator, recordIdParamsValidator],
|
|
146
|
+
bodyValidator: crudResource.operations.patch.bodyValidator,
|
|
147
|
+
responseValidators: withStandardErrorResponses(
|
|
148
|
+
{
|
|
149
|
+
200: crudResource.operations.patch.outputValidator
|
|
150
|
+
},
|
|
151
|
+
{ includeValidation400: true }
|
|
152
|
+
)
|
|
153
|
+
},
|
|
154
|
+
async function (request, reply) {
|
|
155
|
+
const response = await request.executeAction({
|
|
156
|
+
actionId: actionIds.update,
|
|
157
|
+
input: {
|
|
158
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
159
|
+
recordId: request.input.params.recordId,
|
|
160
|
+
patch: request.input.body
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
reply.code(200).send(response);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
router.register(
|
|
168
|
+
"DELETE",
|
|
169
|
+
`${routeBase}/:recordId`,
|
|
170
|
+
{
|
|
171
|
+
auth: "required",
|
|
172
|
+
surface: normalizedRouteSurface,
|
|
173
|
+
visibility: routeVisibility,
|
|
174
|
+
meta: {
|
|
175
|
+
tags: ["crud"],
|
|
176
|
+
summary: "Delete a record."
|
|
177
|
+
},
|
|
178
|
+
paramsValidator: [routeParamsValidator, recordIdParamsValidator],
|
|
179
|
+
responseValidators: withStandardErrorResponses({
|
|
180
|
+
200: crudResource.operations.delete.outputValidator
|
|
181
|
+
})
|
|
182
|
+
},
|
|
183
|
+
async function (request, reply) {
|
|
184
|
+
const response = await request.executeAction({
|
|
185
|
+
actionId: actionIds.delete,
|
|
186
|
+
input: {
|
|
187
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
188
|
+
recordId: request.input.params.recordId
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
reply.code(200).send(response);
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { registerRoutes };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createRepository } from "../../../../src/server/repository.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
|
|
3
|
+
const serviceEvents = Object.freeze({
|
|
4
|
+
createRecord: Object.freeze([
|
|
5
|
+
Object.freeze({
|
|
6
|
+
type: "entity.changed",
|
|
7
|
+
source: "crud",
|
|
8
|
+
entity: "record",
|
|
9
|
+
operation: "created",
|
|
10
|
+
entityId: ({ result }) => result?.id,
|
|
11
|
+
realtime: Object.freeze({
|
|
12
|
+
event: "${option:namespace|snake}.record.changed",
|
|
13
|
+
audience: "event_scope"
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
]),
|
|
17
|
+
updateRecord: Object.freeze([
|
|
18
|
+
Object.freeze({
|
|
19
|
+
type: "entity.changed",
|
|
20
|
+
source: "crud",
|
|
21
|
+
entity: "record",
|
|
22
|
+
operation: "updated",
|
|
23
|
+
entityId: ({ result }) => result?.id,
|
|
24
|
+
realtime: Object.freeze({
|
|
25
|
+
event: "${option:namespace|snake}.record.changed",
|
|
26
|
+
audience: "event_scope"
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
]),
|
|
30
|
+
deleteRecord: Object.freeze([
|
|
31
|
+
Object.freeze({
|
|
32
|
+
type: "entity.changed",
|
|
33
|
+
source: "crud",
|
|
34
|
+
entity: "record",
|
|
35
|
+
operation: "deleted",
|
|
36
|
+
entityId: ({ result }) => result?.id,
|
|
37
|
+
realtime: Object.freeze({
|
|
38
|
+
event: "${option:namespace|snake}.record.changed",
|
|
39
|
+
audience: "event_scope"
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
])
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function createService({ ${option:namespace|camel}Repository } = {}) {
|
|
46
|
+
if (!${option:namespace|camel}Repository) {
|
|
47
|
+
throw new Error("${option:namespace|camel}Service requires ${option:namespace|camel}Repository.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function listRecords(query = {}, options = {}) {
|
|
51
|
+
return ${option:namespace|camel}Repository.list(query, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getRecord(recordId, options = {}) {
|
|
55
|
+
const record = await ${option:namespace|camel}Repository.findById(recordId, options);
|
|
56
|
+
if (!record) {
|
|
57
|
+
throw new AppError(404, "Record not found.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return record;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createRecord(payload = {}, options = {}) {
|
|
64
|
+
const record = await ${option:namespace|camel}Repository.create(payload, options);
|
|
65
|
+
if (!record) {
|
|
66
|
+
throw new Error("${option:namespace|camel}Service could not load the created record.");
|
|
67
|
+
}
|
|
68
|
+
return record;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function updateRecord(recordId, payload = {}, options = {}) {
|
|
72
|
+
const record = await ${option:namespace|camel}Repository.updateById(recordId, payload, options);
|
|
73
|
+
if (!record) {
|
|
74
|
+
throw new AppError(404, "Record not found.");
|
|
75
|
+
}
|
|
76
|
+
return record;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function deleteRecord(recordId, options = {}) {
|
|
80
|
+
const deleted = await ${option:namespace|camel}Repository.deleteById(recordId, options);
|
|
81
|
+
if (!deleted) {
|
|
82
|
+
throw new AppError(404, "Record not found.");
|
|
83
|
+
}
|
|
84
|
+
return deleted;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
listRecords,
|
|
89
|
+
getRecord,
|
|
90
|
+
createRecord,
|
|
91
|
+
updateRecord,
|
|
92
|
+
deleteRecord
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { createService, serviceEvents };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { crudResource } from "../../../../src/shared/crud/crudResource.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { crudResource } from "./${option:namespace|singular|camel}Resource.js";
|
|
2
|
+
export {
|
|
3
|
+
CRUD_MODULE_OWNERSHIP_FILTER_AUTO,
|
|
4
|
+
crudModuleConfig,
|
|
5
|
+
resolveCrudModulePolicy,
|
|
6
|
+
resolveCrudModulePolicyFromAppConfig,
|
|
7
|
+
resolveCrudModulePolicyFromPlacementContext
|
|
8
|
+
} from "./moduleConfig.js";
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import {
|
|
4
|
+
USERS_ROUTE_VISIBILITY_LEVELS,
|
|
5
|
+
normalizeScopedRouteVisibility,
|
|
6
|
+
isWorkspaceVisibility
|
|
7
|
+
} from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
8
|
+
|
|
9
|
+
const CRUD_MODULE_OWNERSHIP_FILTER_AUTO = "auto";
|
|
10
|
+
const CRUD_MODULE_OWNERSHIP_FILTER_SET = new Set([
|
|
11
|
+
...USERS_ROUTE_VISIBILITY_LEVELS,
|
|
12
|
+
CRUD_MODULE_OWNERSHIP_FILTER_AUTO
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const crudModuleConfig = Object.freeze({
|
|
16
|
+
namespace: "${option:namespace|snake}",
|
|
17
|
+
surface: "${option:surface|lower}",
|
|
18
|
+
ownershipFilter: "${option:ownership-filter}",
|
|
19
|
+
relativePath: "/${option:directory-prefix|pathprefix}${option:namespace|kebab}"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeCrudOwnershipFilter(value, { fallback = CRUD_MODULE_OWNERSHIP_FILTER_AUTO } = {}) {
|
|
31
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
32
|
+
if (CRUD_MODULE_OWNERSHIP_FILTER_SET.has(normalized)) {
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizedFallback = normalizeText(fallback).toLowerCase();
|
|
37
|
+
if (CRUD_MODULE_OWNERSHIP_FILTER_SET.has(normalizedFallback)) {
|
|
38
|
+
return normalizedFallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return CRUD_MODULE_OWNERSHIP_FILTER_AUTO;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeCrudRelativePath(value, { context = "resolveCrudModulePolicy" } = {}) {
|
|
45
|
+
const normalized = normalizeText(value);
|
|
46
|
+
if (!normalized) {
|
|
47
|
+
throw new TypeError(`${context} requires a non-empty relativePath.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
51
|
+
const compacted = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
52
|
+
return compacted === "/" ? "/" : compacted.replace(/\/+$/, "") || "/";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeCrudSurfaceDefinitions(sourceDefinitions = {}) {
|
|
56
|
+
const definitions = asRecord(sourceDefinitions);
|
|
57
|
+
const normalized = {};
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(definitions)) {
|
|
60
|
+
const definition = asRecord(value);
|
|
61
|
+
const surfaceId = normalizeSurfaceId(definition.id || key);
|
|
62
|
+
if (!surfaceId) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
normalized[surfaceId] = Object.freeze({
|
|
67
|
+
...definition,
|
|
68
|
+
id: surfaceId,
|
|
69
|
+
enabled: definition.enabled !== false,
|
|
70
|
+
requiresAuth: definition.requiresAuth === true,
|
|
71
|
+
requiresWorkspace: definition.requiresWorkspace === true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Object.freeze(normalized);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveOwnershipFilterForSurfaceDefinition(definition = {}) {
|
|
79
|
+
if (definition.requiresWorkspace === true) {
|
|
80
|
+
return "workspace";
|
|
81
|
+
}
|
|
82
|
+
if (definition.requiresAuth === true) {
|
|
83
|
+
return "user";
|
|
84
|
+
}
|
|
85
|
+
return "public";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveCrudModulePolicy({
|
|
89
|
+
moduleConfig = crudModuleConfig,
|
|
90
|
+
surfaceDefinitions = {},
|
|
91
|
+
defaultSurfaceId = "",
|
|
92
|
+
context = "resolveCrudModulePolicy"
|
|
93
|
+
} = {}) {
|
|
94
|
+
const config = asRecord(moduleConfig);
|
|
95
|
+
const normalizedDefinitions = normalizeCrudSurfaceDefinitions(surfaceDefinitions);
|
|
96
|
+
const requestedSurfaceId = normalizeSurfaceId(config.surface);
|
|
97
|
+
const fallbackSurfaceId = normalizeSurfaceId(defaultSurfaceId);
|
|
98
|
+
const selectedSurfaceId = requestedSurfaceId || fallbackSurfaceId;
|
|
99
|
+
if (!selectedSurfaceId) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${context} requires crudModuleConfig.surface or an app default surface id.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const surfaceDefinition = normalizedDefinitions[selectedSurfaceId];
|
|
106
|
+
if (!surfaceDefinition) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${context} cannot resolve surface "${selectedSurfaceId}" from surface definitions.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (surfaceDefinition.enabled === false) {
|
|
112
|
+
throw new Error(`${context} surface "${selectedSurfaceId}" is disabled.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const requestedOwnershipFilter = normalizeCrudOwnershipFilter(config.ownershipFilter);
|
|
116
|
+
const resolvedOwnershipFilter =
|
|
117
|
+
requestedOwnershipFilter === CRUD_MODULE_OWNERSHIP_FILTER_AUTO
|
|
118
|
+
? resolveOwnershipFilterForSurfaceDefinition(surfaceDefinition)
|
|
119
|
+
: normalizeScopedRouteVisibility(requestedOwnershipFilter, {
|
|
120
|
+
fallback: "public"
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (isWorkspaceVisibility(resolvedOwnershipFilter) && surfaceDefinition.requiresWorkspace !== true) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${context} ownershipFilter "${resolvedOwnershipFilter}" requires a workspace-enabled surface.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const relativePath = normalizeCrudRelativePath(config.relativePath, {
|
|
130
|
+
context
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
namespace: normalizeText(config.namespace).toLowerCase(),
|
|
135
|
+
relativePath,
|
|
136
|
+
surfaceId: selectedSurfaceId,
|
|
137
|
+
requestedOwnershipFilter,
|
|
138
|
+
ownershipFilter: resolvedOwnershipFilter,
|
|
139
|
+
workspaceScoped: isWorkspaceVisibility(resolvedOwnershipFilter),
|
|
140
|
+
surfaceDefinition
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveCrudModulePolicyFromAppConfig(appConfig = {}, options = {}) {
|
|
145
|
+
const config = asRecord(appConfig);
|
|
146
|
+
return resolveCrudModulePolicy({
|
|
147
|
+
...asRecord(options),
|
|
148
|
+
surfaceDefinitions: config.surfaceDefinitions,
|
|
149
|
+
defaultSurfaceId: config.surfaceDefaultId
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveCrudModulePolicyFromPlacementContext(placementContext = null, options = {}) {
|
|
154
|
+
const context = asRecord(placementContext);
|
|
155
|
+
const surfaceConfig = asRecord(context.surfaceConfig);
|
|
156
|
+
return resolveCrudModulePolicy({
|
|
157
|
+
...asRecord(options),
|
|
158
|
+
surfaceDefinitions: surfaceConfig.surfacesById,
|
|
159
|
+
defaultSurfaceId: surfaceConfig.defaultSurfaceId
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
CRUD_MODULE_OWNERSHIP_FILTER_AUTO,
|
|
165
|
+
crudModuleConfig,
|
|
166
|
+
resolveCrudModulePolicy,
|
|
167
|
+
resolveCrudModulePolicyFromAppConfig,
|
|
168
|
+
resolveCrudModulePolicyFromPlacementContext
|
|
169
|
+
};
|