@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,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 };
|