@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.
Files changed (40) hide show
  1. package/package.descriptor.mjs +322 -0
  2. package/package.json +22 -0
  3. package/src/client/index.js +3 -0
  4. package/src/server/CrudServiceProvider.js +11 -0
  5. package/src/server/actionIds.js +22 -0
  6. package/src/server/actions.js +152 -0
  7. package/src/server/registerRoutes.js +235 -0
  8. package/src/server/repository.js +162 -0
  9. package/src/server/service.js +96 -0
  10. package/src/shared/crud/crudModuleConfig.js +310 -0
  11. package/src/shared/crud/crudResource.js +191 -0
  12. package/src/shared/index.js +12 -0
  13. package/templates/migrations/crud_initial.cjs +42 -0
  14. package/templates/src/elements/CreateElement.vue +115 -0
  15. package/templates/src/elements/EditElement.vue +140 -0
  16. package/templates/src/elements/ListElement.vue +88 -0
  17. package/templates/src/elements/ViewElement.vue +126 -0
  18. package/templates/src/elements/clientSupport.js +41 -0
  19. package/templates/src/local-package/client/index.js +4 -0
  20. package/templates/src/local-package/package.descriptor.mjs +83 -0
  21. package/templates/src/local-package/package.json +14 -0
  22. package/templates/src/local-package/server/CrudServiceProvider.js +87 -0
  23. package/templates/src/local-package/server/actionIds.js +9 -0
  24. package/templates/src/local-package/server/actions.js +151 -0
  25. package/templates/src/local-package/server/diTokens.js +4 -0
  26. package/templates/src/local-package/server/registerRoutes.js +196 -0
  27. package/templates/src/local-package/server/repository.js +1 -0
  28. package/templates/src/local-package/server/service.js +96 -0
  29. package/templates/src/local-package/shared/crudResource.js +1 -0
  30. package/templates/src/local-package/shared/index.js +8 -0
  31. package/templates/src/local-package/shared/moduleConfig.js +169 -0
  32. package/templates/src/pages/admin/crud/[recordId]/edit.vue +7 -0
  33. package/templates/src/pages/admin/crud/[recordId]/index.vue +7 -0
  34. package/templates/src/pages/admin/crud/index.vue +7 -0
  35. package/templates/src/pages/admin/crud/new.vue +7 -0
  36. package/test/crudModuleConfig.test.js +225 -0
  37. package/test/crudResource.test.js +41 -0
  38. package/test/crudServerGuards.test.js +61 -0
  39. package/test/crudService.test.js +83 -0
  40. package/test/routeInputContracts.test.js +211 -0
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <section class="crud-edit-element d-flex flex-column ga-4">
3
+ <v-card rounded="lg" elevation="1" border>
4
+ <v-card-item>
5
+ <div class="d-flex align-center ga-3 flex-wrap w-100">
6
+ <div>
7
+ <v-card-title class="px-0">Edit ${option:namespace|singular|pascal|default(Record)}</v-card-title>
8
+ <v-card-subtitle class="px-0">Update the selected ${option:namespace|singular|default(record)}.</v-card-subtitle>
9
+ </div>
10
+ <v-spacer />
11
+ <v-btn variant="text" :to="viewPath || listPath">Cancel</v-btn>
12
+ <v-btn
13
+ color="primary"
14
+ :loading="addEdit.isSaving"
15
+ :disabled="addEdit.isInitialLoading || addEdit.isRefetching || !addEdit.canSave"
16
+ @click="addEdit.submit"
17
+ >
18
+ Save changes
19
+ </v-btn>
20
+ </div>
21
+ </v-card-item>
22
+ <v-divider />
23
+ <v-card-text class="pt-4">
24
+ <p v-if="addEdit.loadError" class="text-body-2 text-medium-emphasis mb-0">
25
+ {{ addEdit.loadError }}
26
+ </p>
27
+ <template v-else-if="showFormSkeleton">
28
+ <v-skeleton-loader type="text@2, list-item-two-line@4, button" />
29
+ </template>
30
+ <v-form v-else @submit.prevent="addEdit.submit" novalidate>
31
+ <v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
32
+ <v-row>
33
+ <v-col cols="12" md="6">
34
+ <v-text-field
35
+ v-model="recordForm.textField"
36
+ label="Text field"
37
+ variant="outlined"
38
+ density="comfortable"
39
+ maxlength="160"
40
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
41
+ :error-messages="addEdit.fieldErrors.textField ? [addEdit.fieldErrors.textField] : []"
42
+ />
43
+ </v-col>
44
+ <v-col cols="12" md="6">
45
+ <v-text-field
46
+ v-model="recordForm.dateField"
47
+ label="Date field"
48
+ type="date"
49
+ variant="outlined"
50
+ density="comfortable"
51
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
52
+ :error-messages="addEdit.fieldErrors.dateField ? [addEdit.fieldErrors.dateField] : []"
53
+ />
54
+ </v-col>
55
+ <v-col cols="12" md="6">
56
+ <v-text-field
57
+ v-model="recordForm.numberField"
58
+ label="Number field"
59
+ type="number"
60
+ variant="outlined"
61
+ density="comfortable"
62
+ :readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
63
+ :error-messages="addEdit.fieldErrors.numberField ? [addEdit.fieldErrors.numberField] : []"
64
+ />
65
+ </v-col>
66
+ </v-row>
67
+ </v-form>
68
+ </v-card-text>
69
+ </v-card>
70
+ </section>
71
+ </template>
72
+
73
+ <script setup>
74
+ import { computed, reactive } from "vue";
75
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
76
+ import { useAddEdit } from "@jskit-ai/users-web/client/composables/useAddEdit";
77
+ import {
78
+ crudResource,
79
+ useCrudRecordRuntime,
80
+ useCrudModulePolicyRuntime
81
+ } from "./clientSupport.js";
82
+
83
+ const {
84
+ listPath,
85
+ recordId,
86
+ viewPath,
87
+ apiSuffix,
88
+ viewQueryKey,
89
+ invalidateAndGoView
90
+ } = useCrudRecordRuntime();
91
+ const { ownershipFilter, surfaceId } = useCrudModulePolicyRuntime();
92
+ const recordForm = reactive({
93
+ textField: "",
94
+ dateField: "",
95
+ numberField: ""
96
+ });
97
+
98
+ function toDateInputValue(value) {
99
+ const normalized = String(value || "").trim();
100
+ if (!normalized) {
101
+ return "";
102
+ }
103
+
104
+ return normalized.slice(0, 10);
105
+ }
106
+
107
+ const addEdit = useAddEdit({
108
+ ownershipFilter,
109
+ surfaceId,
110
+ resource: crudResource,
111
+ apiSuffix,
112
+ queryKeyFactory: viewQueryKey,
113
+ writeMethod: "PATCH",
114
+ fallbackLoadError: "Unable to load record.",
115
+ fallbackSaveError: "Unable to save record.",
116
+ fieldErrorKeys: ["textField", "dateField", "numberField"],
117
+ model: recordForm,
118
+ parseInput: (rawPayload) =>
119
+ validateOperationSection({
120
+ operation: crudResource.operations.patch,
121
+ section: "bodyValidator",
122
+ value: rawPayload
123
+ }),
124
+ mapLoadedToModel: (model, payload = {}) => {
125
+ model.textField = String(payload?.textField || "");
126
+ model.dateField = toDateInputValue(payload?.dateField);
127
+ model.numberField = String(payload?.numberField ?? "");
128
+ },
129
+ buildRawPayload: (model) => ({
130
+ textField: model.textField,
131
+ dateField: model.dateField,
132
+ numberField: model.numberField
133
+ }),
134
+ onSaveSuccess: async (payload, { queryClient }) => {
135
+ await invalidateAndGoView(queryClient, payload?.id || recordId.value);
136
+ }
137
+ });
138
+
139
+ const showFormSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
140
+ </script>
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <section class="crud-list-element d-flex flex-column ga-4">
3
+ <v-card rounded="lg" elevation="1" border>
4
+ <v-card-item>
5
+ <div class="d-flex align-center ga-3 flex-wrap w-100">
6
+ <div>
7
+ <v-card-title class="px-0">${option:namespace|plural|pascal}</v-card-title>
8
+ <v-card-subtitle class="px-0">Manage ${option:namespace|plural|default(records)}.</v-card-subtitle>
9
+ </div>
10
+ <v-spacer />
11
+ <v-btn variant="outlined" :loading="isFetching" @click="records.reload">Refresh</v-btn>
12
+ <v-btn color="primary" :to="createPath">New ${option:namespace|singular|default(record)}</v-btn>
13
+ </div>
14
+ </v-card-item>
15
+ <v-divider />
16
+ <v-card-text class="pt-4">
17
+ <template v-if="showListSkeleton">
18
+ <v-skeleton-loader type="text@2, list-item-two-line@5" />
19
+ </template>
20
+ <template v-else>
21
+ <v-progress-linear v-if="isRefetching" indeterminate class="mb-3" />
22
+
23
+ <v-table density="comfortable">
24
+ <thead>
25
+ <tr>
26
+ <th>Text field</th>
27
+ <th>Date field</th>
28
+ <th>Number field</th>
29
+ <th>Updated</th>
30
+ <th class="text-right">Actions</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <tr v-if="items.length < 1">
35
+ <td colspan="5" class="text-center py-6 text-medium-emphasis">No records yet.</td>
36
+ </tr>
37
+ <tr v-for="record in items" :key="record.id">
38
+ <td>{{ record.textField }}</td>
39
+ <td>{{ crudContext.formatDateTime(record.dateField) }}</td>
40
+ <td>{{ record.numberField }}</td>
41
+ <td>{{ crudContext.formatDateTime(record.updatedAt) }}</td>
42
+ <td class="text-right">
43
+ <v-btn size="small" variant="text" :to="crudContext.resolveViewPath(record.id)">
44
+ Open
45
+ </v-btn>
46
+ </td>
47
+ </tr>
48
+ </tbody>
49
+ </v-table>
50
+
51
+ <div v-if="hasMore" class="d-flex justify-center pt-4">
52
+ <v-btn variant="text" :loading="isLoadingMore" @click="records.loadMore">Load more</v-btn>
53
+ </div>
54
+ </template>
55
+ </v-card-text>
56
+ </v-card>
57
+ </section>
58
+ </template>
59
+
60
+ <script setup>
61
+ import { computed } from "vue";
62
+ import { useList } from "@jskit-ai/users-web/client/composables/useList";
63
+ import { useCrudListRuntime, useCrudModulePolicyRuntime } from "./clientSupport.js";
64
+
65
+ const {
66
+ crudContext,
67
+ createPath,
68
+ apiSuffix,
69
+ listQueryKey
70
+ } = useCrudListRuntime();
71
+ const { ownershipFilter, surfaceId } = useCrudModulePolicyRuntime();
72
+
73
+ const records = useList({
74
+ ownershipFilter,
75
+ surfaceId,
76
+ apiSuffix,
77
+ queryKeyFactory: listQueryKey,
78
+ fallbackLoadError: "Unable to load records."
79
+ });
80
+
81
+ const items = records.items;
82
+ const isLoading = records.isInitialLoading;
83
+ const isFetching = records.isFetching;
84
+ const isRefetching = records.isRefetching;
85
+ const hasMore = records.hasMore;
86
+ const isLoadingMore = records.isLoadingMore;
87
+ const showListSkeleton = computed(() => Boolean(isLoading.value && items.value.length < 1));
88
+ </script>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <section class="crud-view-element d-flex flex-column ga-4">
3
+ <v-card rounded="lg" elevation="1" border>
4
+ <v-card-item>
5
+ <div class="d-flex align-center ga-3 flex-wrap w-100">
6
+ <div>
7
+ <v-card-title class="px-0">{{ title }}</v-card-title>
8
+ <v-card-subtitle class="px-0">View and manage this ${option:namespace|singular|default(record)}.</v-card-subtitle>
9
+ </div>
10
+ <v-spacer />
11
+ <v-btn variant="text" :to="listPath">Back to ${option:namespace|plural|default(records)}</v-btn>
12
+ <v-btn color="primary" variant="outlined" :to="editPath" :disabled="!editPath">Edit</v-btn>
13
+ <v-btn color="error" variant="tonal" :loading="deleteCommand.isRunning" @click="confirmDelete">Delete</v-btn>
14
+ </div>
15
+ </v-card-item>
16
+ <v-divider />
17
+ <v-card-text class="pt-4">
18
+ <div v-if="view.loadError.value || view.isNotFound.value" class="text-body-2 text-medium-emphasis py-2">
19
+ Record unavailable.
20
+ </div>
21
+
22
+ <template v-else-if="view.isLoading.value">
23
+ <v-skeleton-loader type="text@2, list-item-two-line@5" />
24
+ </template>
25
+
26
+ <template v-else>
27
+ <v-progress-linear v-if="view.isRefetching.value" indeterminate class="mb-4" />
28
+ <v-row>
29
+ <v-col cols="12" md="6">
30
+ <div class="text-caption text-medium-emphasis">Text field</div>
31
+ <div class="text-body-1">{{ record.textField }}</div>
32
+ </v-col>
33
+ <v-col cols="12" md="6">
34
+ <div class="text-caption text-medium-emphasis">Date field</div>
35
+ <div class="text-body-1">{{ crudContext.formatDateTime(record.dateField) }}</div>
36
+ </v-col>
37
+ <v-col cols="12" md="6">
38
+ <div class="text-caption text-medium-emphasis">Number field</div>
39
+ <div class="text-body-1">{{ record.numberField }}</div>
40
+ </v-col>
41
+ <v-col cols="12" md="6">
42
+ <div class="text-caption text-medium-emphasis">Created</div>
43
+ <div class="text-body-1">{{ crudContext.formatDateTime(record.createdAt) }}</div>
44
+ </v-col>
45
+ <v-col cols="12" md="6">
46
+ <div class="text-caption text-medium-emphasis">Updated</div>
47
+ <div class="text-body-1">{{ crudContext.formatDateTime(record.updatedAt) }}</div>
48
+ </v-col>
49
+ </v-row>
50
+ </template>
51
+
52
+ </v-card-text>
53
+ </v-card>
54
+ </section>
55
+ </template>
56
+
57
+ <script setup>
58
+ import { computed, reactive } from "vue";
59
+ import { useCommand } from "@jskit-ai/users-web/client/composables/useCommand";
60
+ import { useView } from "@jskit-ai/users-web/client/composables/useView";
61
+ import { useCrudRecordRuntime, useCrudModulePolicyRuntime } from "./clientSupport.js";
62
+
63
+ const {
64
+ crudContext,
65
+ listPath,
66
+ recordId,
67
+ editPath,
68
+ apiSuffix,
69
+ viewQueryKey,
70
+ invalidateAndGoList
71
+ } = useCrudRecordRuntime();
72
+ const { ownershipFilter, surfaceId } = useCrudModulePolicyRuntime();
73
+ const record = reactive({
74
+ id: 0,
75
+ textField: "",
76
+ dateField: "",
77
+ numberField: 0,
78
+ createdAt: "",
79
+ updatedAt: ""
80
+ });
81
+
82
+ const title = computed(() => {
83
+ const textField = String(record.textField || "").trim();
84
+ return textField || "${option:namespace|singular|pascal|default(Record)}";
85
+ });
86
+
87
+ const view = useView({
88
+ ownershipFilter,
89
+ surfaceId,
90
+ apiSuffix,
91
+ queryKeyFactory: viewQueryKey,
92
+ fallbackLoadError: "Unable to load record.",
93
+ notFoundMessage: "Record not found.",
94
+ model: record,
95
+ mapLoadedToModel: (model, payload = {}) => {
96
+ model.id = Number(payload.id || 0);
97
+ model.textField = String(payload.textField || "");
98
+ model.dateField = String(payload.dateField || "");
99
+ model.numberField = Number(payload.numberField || 0);
100
+ model.createdAt = String(payload.createdAt || "");
101
+ model.updatedAt = String(payload.updatedAt || "");
102
+ }
103
+ });
104
+ const deleteCommand = useCommand({
105
+ ownershipFilter,
106
+ surfaceId,
107
+ apiSuffix,
108
+ writeMethod: "DELETE",
109
+ fallbackRunError: "Unable to delete record.",
110
+ messages: {
111
+ success: "Record deleted.",
112
+ error: "Unable to delete record."
113
+ },
114
+ onRunSuccess: async (_, { queryClient }) => {
115
+ await invalidateAndGoList(queryClient);
116
+ }
117
+ });
118
+
119
+ async function confirmDelete() {
120
+ if (!window.confirm("Delete this record?")) {
121
+ return;
122
+ }
123
+
124
+ await deleteCommand.run();
125
+ }
126
+ </script>
@@ -0,0 +1,41 @@
1
+ import { createCrudClientSupport } from "@jskit-ai/crud-core/client";
2
+ import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
3
+ import { crudResource } from "../shared/${option:namespace|singular|camel}Resource.js";
4
+ import {
5
+ crudModuleConfig,
6
+ resolveCrudModulePolicyFromPlacementContext
7
+ } from "../shared/moduleConfig.js";
8
+
9
+ const crudClientSupport = createCrudClientSupport(crudModuleConfig);
10
+
11
+ const {
12
+ useCrudClientContext,
13
+ useCrudListRuntime,
14
+ useCrudCreateRuntime,
15
+ useCrudRecordRuntime,
16
+ toRouteRecordId
17
+ } = crudClientSupport;
18
+
19
+ function useCrudModulePolicyRuntime() {
20
+ const paths = usePaths();
21
+ const modulePolicy = resolveCrudModulePolicyFromPlacementContext(paths.placementContext.value, {
22
+ moduleConfig: crudModuleConfig,
23
+ context: "crud client runtime"
24
+ });
25
+
26
+ return Object.freeze({
27
+ modulePolicy,
28
+ surfaceId: modulePolicy.surfaceId,
29
+ ownershipFilter: modulePolicy.ownershipFilter
30
+ });
31
+ }
32
+
33
+ export {
34
+ crudResource,
35
+ useCrudModulePolicyRuntime,
36
+ useCrudClientContext,
37
+ useCrudListRuntime,
38
+ useCrudCreateRuntime,
39
+ useCrudRecordRuntime,
40
+ toRouteRecordId
41
+ };
@@ -0,0 +1,4 @@
1
+ export { default as List${option:namespace|plural|pascal}Element } from "./List${option:namespace|plural|pascal}Element.vue";
2
+ export { default as View${option:namespace|singular|pascal}Element } from "./View${option:namespace|singular|pascal}Element.vue";
3
+ export { default as Create${option:namespace|singular|pascal}Element } from "./Create${option:namespace|singular|pascal}Element.vue";
4
+ export { default as Edit${option:namespace|singular|pascal}Element } from "./Edit${option:namespace|singular|pascal}Element.vue";
@@ -0,0 +1,83 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@local/${option:namespace|kebab}",
4
+ version: "0.1.0",
5
+ description: "App-local CRUD package (${option:namespace|kebab}).",
6
+ dependsOn: [
7
+ "@jskit-ai/auth-core",
8
+ "@jskit-ai/crud-core",
9
+ "@jskit-ai/database-runtime",
10
+ "@jskit-ai/http-runtime",
11
+ "@jskit-ai/realtime",
12
+ "@jskit-ai/shell-web",
13
+ "@jskit-ai/users-core",
14
+ "@jskit-ai/users-web"
15
+ ],
16
+ capabilities: {
17
+ provides: [
18
+ "crud.${option:namespace|kebab}"
19
+ ],
20
+ requires: [
21
+ "runtime.actions",
22
+ "runtime.database",
23
+ "auth.policy",
24
+ "users.core",
25
+ "users.web",
26
+ "runtime.web-placement",
27
+ "runtime.realtime.client"
28
+ ]
29
+ },
30
+ runtime: {
31
+ server: {
32
+ providers: [
33
+ {
34
+ entrypoint: "src/server/${option:namespace|pascal}ServiceProvider.js",
35
+ export: "${option:namespace|pascal}ServiceProvider"
36
+ }
37
+ ]
38
+ },
39
+ client: {
40
+ providers: []
41
+ }
42
+ },
43
+ metadata: {
44
+ apiSummary: {
45
+ surfaces: [
46
+ {
47
+ subpath: "./server/diTokens",
48
+ summary: "App-local CRUD public server DI token constants."
49
+ },
50
+ {
51
+ subpath: "./server/actionIds",
52
+ summary: "App-local CRUD public action identifiers."
53
+ },
54
+ {
55
+ subpath: "./shared",
56
+ summary: "App-local CRUD shared resource."
57
+ },
58
+ {
59
+ subpath: "./client/*",
60
+ summary: "App-local CRUD Vue client elements."
61
+ }
62
+ ],
63
+ containerTokens: {
64
+ server: [
65
+ "repository.${option:namespace|snake}",
66
+ "crud.${option:namespace|snake}"
67
+ ],
68
+ client: []
69
+ }
70
+ }
71
+ },
72
+ mutations: {
73
+ dependencies: {
74
+ runtime: {},
75
+ dev: {}
76
+ },
77
+ packageJson: {
78
+ scripts: {}
79
+ },
80
+ procfile: {},
81
+ files: []
82
+ }
83
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@local/${option:namespace|kebab}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ "./client": "./src/client/index.js",
8
+ "./client/*": "./src/client/*.vue",
9
+ "./client/clientSupport": "./src/client/clientSupport.js",
10
+ "./server/diTokens": "./src/server/diTokens.js",
11
+ "./server/actionIds": "./src/server/actionIds.js",
12
+ "./shared": "./src/shared/index.js"
13
+ }
14
+ }
@@ -0,0 +1,87 @@
1
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
2
+ import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
3
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
4
+ import { createRepository } from "./repository.js";
5
+ import {
6
+ createService,
7
+ serviceEvents
8
+ } from "./service.js";
9
+ import { createActions } from "./actions.js";
10
+ import { registerRoutes } from "./registerRoutes.js";
11
+ import {
12
+ crudModuleConfig,
13
+ resolveCrudModulePolicyFromAppConfig
14
+ } from "../shared/moduleConfig.js";
15
+ import {
16
+ NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN,
17
+ NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN
18
+ } from "./diTokens.js";
19
+
20
+ const NAMESPACE_${option:namespace|snake|upper}_PROVIDER_ID = NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN;
21
+ const NAMESPACE_${option:namespace|snake|upper}_TABLE_NAME = "crud_${option:namespace|snake}";
22
+
23
+ function resolveCrudPolicyFromApp(app) {
24
+ return resolveCrudModulePolicyFromAppConfig(resolveAppConfig(app), {
25
+ moduleConfig: crudModuleConfig,
26
+ context: "${option:namespace|pascal}ServiceProvider"
27
+ });
28
+ }
29
+
30
+ class ${option:namespace|pascal}ServiceProvider {
31
+ static id = NAMESPACE_${option:namespace|snake|upper}_PROVIDER_ID;
32
+
33
+ static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main", "users.core"];
34
+
35
+ register(app) {
36
+ if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
37
+ throw new Error("${option:namespace|pascal}ServiceProvider requires application singleton()/service()/actions().");
38
+ }
39
+
40
+ const crudPolicy = resolveCrudPolicyFromApp(app);
41
+
42
+ app.singleton(NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN, (scope) => {
43
+ const knex = scope.make(KERNEL_TOKENS.Knex);
44
+ return createRepository(knex, {
45
+ tableName: NAMESPACE_${option:namespace|snake|upper}_TABLE_NAME
46
+ });
47
+ });
48
+
49
+ app.service(
50
+ NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN,
51
+ (scope) => {
52
+ return createService({
53
+ ${option:namespace|camel}Repository: scope.make(NAMESPACE_${option:namespace|snake|upper}_REPOSITORY_TOKEN)
54
+ });
55
+ },
56
+ {
57
+ events: serviceEvents
58
+ }
59
+ );
60
+
61
+ app.actions(
62
+ withActionDefaults(
63
+ createActions({
64
+ surface: crudPolicy.surfaceId
65
+ }),
66
+ {
67
+ domain: "crud",
68
+ dependencies: {
69
+ ${option:namespace|camel}Service: NAMESPACE_${option:namespace|snake|upper}_SERVICE_TOKEN
70
+ }
71
+ }
72
+ )
73
+ );
74
+ }
75
+
76
+ boot(app) {
77
+ const crudPolicy = resolveCrudPolicyFromApp(app);
78
+ registerRoutes(app, {
79
+ routeOwnershipFilter: crudPolicy.ownershipFilter,
80
+ routeSurface: crudPolicy.surfaceId,
81
+ routeSurfaceRequiresWorkspace: crudPolicy.surfaceDefinition.requiresWorkspace === true,
82
+ routeRelativePath: crudPolicy.relativePath
83
+ });
84
+ }
85
+ }
86
+
87
+ export { ${option:namespace|pascal}ServiceProvider };
@@ -0,0 +1,9 @@
1
+ const actionIds = Object.freeze({
2
+ list: "crud.${option:namespace|snake}.list",
3
+ view: "crud.${option:namespace|snake}.view",
4
+ create: "crud.${option:namespace|snake}.create",
5
+ update: "crud.${option:namespace|snake}.update",
6
+ delete: "crud.${option:namespace|snake}.delete"
7
+ });
8
+
9
+ export { actionIds };