@jskit-ai/crud-server-generator 0.1.26 → 0.1.28
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 +19 -14
- package/package.json +9 -7
- package/src/server/{CrudServiceProvider.js → CrudProvider.js} +2 -2
- package/src/server/buildTemplateContext.js +278 -32
- package/src/server/subcommands/addField.js +238 -0
- package/src/server/subcommands/resourceAst.js +632 -0
- package/src/shared/crud/crudResource.js +93 -98
- package/templates/migrations/crud_initial.cjs +1 -0
- package/templates/src/local-package/package.descriptor.mjs +2 -2
- package/templates/src/local-package/server/{CrudServiceProvider.js → CrudProvider.js} +13 -8
- package/templates/src/local-package/server/actions.js +24 -10
- package/templates/src/local-package/server/registerRoutes.js +32 -33
- package/templates/src/local-package/server/repository.js +33 -132
- package/templates/src/local-package/server/service.js +88 -47
- package/templates/src/local-package/shared/crudResource.js +77 -45
- package/test/addFieldSubcommand.test.js +167 -0
- package/test/buildTemplateContext.test.js +198 -4
- package/test/crudResource.test.js +6 -0
- package/test/crudServerGuards.test.js +43 -49
- package/test/crudService.test.js +93 -5
- package/test/routeInputContracts.test.js +144 -41
- package/test-support/templateServerFixture.js +169 -0
- package/src/server/actionIds.js +0 -22
- package/src/server/actions.js +0 -152
- package/src/server/registerRoutes.js +0 -234
- package/src/server/repository.js +0 -162
- package/src/server/service.js +0 -96
|
@@ -1,157 +1,58 @@
|
|
|
1
|
-
import { toInsertDateTime } from "@jskit-ai/database-runtime/shared";
|
|
2
|
-
import { applyVisibility, applyVisibilityOwners } from "@jskit-ai/database-runtime/shared/visibility";
|
|
3
1
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@jskit-ai/crud-core/server/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
createCrudRepositoryRuntime,
|
|
3
|
+
crudRepositoryList,
|
|
4
|
+
crudRepositoryFindById,
|
|
5
|
+
crudRepositoryListByIds,
|
|
6
|
+
crudRepositoryCreate,
|
|
7
|
+
crudRepositoryUpdateById,
|
|
8
|
+
crudRepositoryDeleteById
|
|
9
|
+
} from "@jskit-ai/crud-core/server/repositoryMethods";
|
|
10
|
+
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
11
|
+
|
|
12
|
+
const LIST_CONFIG = Object.freeze({
|
|
13
|
+
// defaultLimit: 20,
|
|
14
|
+
// maxLimit: 100,
|
|
15
|
+
// searchColumns: ["name"]
|
|
16
|
+
});
|
|
19
17
|
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
writeMappings: WRITE_MAPPINGS
|
|
24
|
-
} = buildRepositoryColumnMetadata({
|
|
25
|
-
outputKeys: OUTPUT_KEYS,
|
|
26
|
-
writeKeys: WRITE_KEYS,
|
|
27
|
-
columnOverrides: COLUMN_OVERRIDES
|
|
18
|
+
const repositoryRuntime = createCrudRepositoryRuntime(resource, {
|
|
19
|
+
context: "${option:namespace|snake} repository",
|
|
20
|
+
list: LIST_CONFIG
|
|
28
21
|
});
|
|
29
22
|
|
|
30
|
-
function createRepository(knex,
|
|
23
|
+
function createRepository(knex, options = {}) {
|
|
31
24
|
if (typeof knex !== "function") {
|
|
32
25
|
throw new TypeError("crudRepository requires knex.");
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async function list({ cursor = 0, limit = DEFAULT_LIST_LIMIT } = {}, options = {}) {
|
|
39
|
-
const client = options?.trx || knex;
|
|
40
|
-
const normalizedCursor = Number.isInteger(Number(cursor)) && Number(cursor) > 0 ? Number(cursor) : 0;
|
|
41
|
-
const normalizedLimit = normalizeCrudListLimit(limit);
|
|
42
|
-
const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
|
|
43
|
-
|
|
44
|
-
let query = client(resolvedTableName)
|
|
45
|
-
.select(...SELECT_COLUMNS)
|
|
46
|
-
.where(visible)
|
|
47
|
-
.orderBy(resolvedIdColumn, "asc")
|
|
48
|
-
.limit(normalizedLimit + 1);
|
|
49
|
-
|
|
50
|
-
if (normalizedCursor > 0) {
|
|
51
|
-
query = query.where(resolvedIdColumn, ">", normalizedCursor);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const rows = await query;
|
|
55
|
-
const hasMore = rows.length > normalizedLimit;
|
|
56
|
-
const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
|
|
57
|
-
const items = pageRows.map((row) => baseMapRecordRow(row, OUTPUT_KEYS, COLUMN_OVERRIDES));
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
items,
|
|
61
|
-
nextCursor: hasMore && items.length > 0 ? String(items[items.length - 1].id) : null
|
|
62
|
-
};
|
|
28
|
+
async function list(query = {}, callOptions = {}) {
|
|
29
|
+
return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions);
|
|
63
30
|
}
|
|
64
31
|
|
|
65
|
-
async function findById(recordId,
|
|
66
|
-
|
|
67
|
-
const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
|
|
68
|
-
const row = await client(resolvedTableName)
|
|
69
|
-
.select(...SELECT_COLUMNS)
|
|
70
|
-
.where(visible)
|
|
71
|
-
.where({
|
|
72
|
-
[resolvedIdColumn]: Number(recordId)
|
|
73
|
-
})
|
|
74
|
-
.first();
|
|
75
|
-
|
|
76
|
-
return baseMapRecordRow(row, OUTPUT_KEYS, COLUMN_OVERRIDES);
|
|
32
|
+
async function findById(recordId, callOptions = {}) {
|
|
33
|
+
return crudRepositoryFindById(repositoryRuntime, knex, recordId, options, callOptions);
|
|
77
34
|
}
|
|
78
35
|
|
|
79
|
-
async function
|
|
80
|
-
|
|
81
|
-
const timestamp = toInsertDateTime();
|
|
82
|
-
const insertPayload = baseBuildWritePayload(payload, WRITE_KEYS, COLUMN_OVERRIDES);
|
|
83
|
-
if (CREATED_AT_COLUMN && !Object.hasOwn(insertPayload, CREATED_AT_COLUMN)) {
|
|
84
|
-
insertPayload[CREATED_AT_COLUMN] = timestamp;
|
|
85
|
-
}
|
|
86
|
-
if (UPDATED_AT_COLUMN && !Object.hasOwn(insertPayload, UPDATED_AT_COLUMN)) {
|
|
87
|
-
insertPayload[UPDATED_AT_COLUMN] = timestamp;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const withOwners = applyVisibilityOwners(insertPayload, options.visibilityContext);
|
|
91
|
-
const [recordId] = await client(resolvedTableName).insert(withOwners);
|
|
92
|
-
|
|
93
|
-
return findById(recordId, {
|
|
94
|
-
...options,
|
|
95
|
-
trx: client
|
|
96
|
-
});
|
|
36
|
+
async function listByIds(ids = [], callOptions = {}) {
|
|
37
|
+
return crudRepositoryListByIds(repositoryRuntime, knex, ids, options, callOptions);
|
|
97
38
|
}
|
|
98
39
|
|
|
99
|
-
async function
|
|
100
|
-
|
|
101
|
-
const dbPatch = baseBuildWritePayload(patch, WRITE_KEYS, COLUMN_OVERRIDES);
|
|
102
|
-
const visible = (queryBuilder) => applyVisibility(queryBuilder, options.visibilityContext);
|
|
103
|
-
if (UPDATED_AT_COLUMN) {
|
|
104
|
-
dbPatch[UPDATED_AT_COLUMN] = toInsertDateTime();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (Object.keys(dbPatch).length < 1) {
|
|
108
|
-
return findById(recordId, {
|
|
109
|
-
...options,
|
|
110
|
-
trx: client
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await client(resolvedTableName)
|
|
115
|
-
.where(visible)
|
|
116
|
-
.where({
|
|
117
|
-
[resolvedIdColumn]: Number(recordId)
|
|
118
|
-
})
|
|
119
|
-
.update(dbPatch);
|
|
120
|
-
|
|
121
|
-
return findById(recordId, {
|
|
122
|
-
...options,
|
|
123
|
-
trx: client
|
|
124
|
-
});
|
|
40
|
+
async function create(payload = {}, callOptions = {}) {
|
|
41
|
+
return crudRepositoryCreate(repositoryRuntime, knex, payload, options, callOptions);
|
|
125
42
|
}
|
|
126
43
|
|
|
127
|
-
async function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const existing = await findById(recordId, {
|
|
131
|
-
...options,
|
|
132
|
-
trx: client
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (!existing) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
await client(resolvedTableName)
|
|
140
|
-
.where(visible)
|
|
141
|
-
.where({
|
|
142
|
-
[resolvedIdColumn]: Number(recordId)
|
|
143
|
-
})
|
|
144
|
-
.delete();
|
|
44
|
+
async function updateById(recordId, patch = {}, callOptions = {}) {
|
|
45
|
+
return crudRepositoryUpdateById(repositoryRuntime, knex, recordId, patch, options, callOptions);
|
|
46
|
+
}
|
|
145
47
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
deleted: true
|
|
149
|
-
};
|
|
48
|
+
async function deleteById(recordId, callOptions = {}) {
|
|
49
|
+
return crudRepositoryDeleteById(repositoryRuntime, knex, recordId, options, callOptions);
|
|
150
50
|
}
|
|
151
51
|
|
|
152
52
|
return Object.freeze({
|
|
153
53
|
list,
|
|
154
54
|
findById,
|
|
55
|
+
listByIds,
|
|
155
56
|
create,
|
|
156
57
|
updateById,
|
|
157
58
|
deleteById
|
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { createCrudServiceEvents } from "@jskit-ai/crud-core/server/serviceEvents";
|
|
3
|
+
import { createCrudFieldAccessRuntime } from "@jskit-ai/crud-core/server/fieldAccess";
|
|
4
|
+
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
5
|
+
|
|
6
|
+
const baseServiceEvents = createCrudServiceEvents(resource, {
|
|
7
|
+
context: "${option:namespace|camel}Service"
|
|
8
|
+
});
|
|
9
|
+
const fieldAccessRuntime = createCrudFieldAccessRuntime(resource, {
|
|
10
|
+
context: "${option:namespace|camel}Service"
|
|
11
|
+
});
|
|
2
12
|
|
|
3
13
|
const serviceEvents = Object.freeze({
|
|
4
|
-
createRecord:
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
])
|
|
14
|
+
createRecord: [...baseServiceEvents.createRecord],
|
|
15
|
+
updateRecord: [...baseServiceEvents.updateRecord],
|
|
16
|
+
deleteRecord: [...baseServiceEvents.deleteRecord]
|
|
43
17
|
});
|
|
44
18
|
|
|
45
|
-
|
|
19
|
+
const DEFAULT_FIELD_ACCESS = Object.freeze({
|
|
20
|
+
// Tip: use createFieldAccessForRoleMatrix(...) from @jskit-ai/crud-core/server/fieldAccess to centralize role matrices.
|
|
21
|
+
// Example:
|
|
22
|
+
// const DEFAULT_FIELD_ACCESS = createFieldAccessForRoleMatrix({
|
|
23
|
+
// default: {
|
|
24
|
+
// readable: { list: ["id", "name"], view: ["id", "name", "email"] },
|
|
25
|
+
// writable: { create: ["name", "email"], update: ["name"] }
|
|
26
|
+
// },
|
|
27
|
+
// admin: {
|
|
28
|
+
// readable: "*",
|
|
29
|
+
// writable: "*"
|
|
30
|
+
// },
|
|
31
|
+
// writeMode: "throw" // or "strip"
|
|
32
|
+
// });
|
|
33
|
+
// readable: ({ action, context }) => ["id", "name"], // null/"*" means no read filtering
|
|
34
|
+
// Read redaction behavior: drop optional fields; use null/default for required fields.
|
|
35
|
+
// writable: ({ action, context }) => ["name"], // null/"*" means no write filtering
|
|
36
|
+
// writeMode: "throw" // "throw" (default) or "strip"
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function createService({ ${option:namespace|camel}Repository, fieldAccess = DEFAULT_FIELD_ACCESS } = {}) {
|
|
46
40
|
if (!${option:namespace|camel}Repository) {
|
|
47
41
|
throw new Error("${option:namespace|camel}Service requires ${option:namespace|camel}Repository.");
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
async function listRecords(query = {}, options = {}) {
|
|
51
|
-
|
|
45
|
+
const result = await ${option:namespace|camel}Repository.list(query, options);
|
|
46
|
+
return fieldAccessRuntime.filterReadableListResult(result, fieldAccess, {
|
|
47
|
+
action: "list",
|
|
48
|
+
query,
|
|
49
|
+
options,
|
|
50
|
+
context: options?.context
|
|
51
|
+
});
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function getRecord(recordId, options = {}) {
|
|
@@ -56,24 +56,50 @@ function createService({ ${option:namespace|camel}Repository } = {}) {
|
|
|
56
56
|
if (!record) {
|
|
57
57
|
throw new AppError(404, "Record not found.");
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
60
|
+
action: "view",
|
|
61
|
+
recordId,
|
|
62
|
+
options,
|
|
63
|
+
context: options?.context
|
|
64
|
+
});
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
async function createRecord(payload = {}, options = {}) {
|
|
64
|
-
const
|
|
68
|
+
const writablePayload = await fieldAccessRuntime.enforceWritablePayload(payload, fieldAccess, {
|
|
69
|
+
action: "create",
|
|
70
|
+
payload,
|
|
71
|
+
options,
|
|
72
|
+
context: options?.context
|
|
73
|
+
});
|
|
74
|
+
const record = await ${option:namespace|camel}Repository.create(writablePayload, options);
|
|
65
75
|
if (!record) {
|
|
66
76
|
throw new Error("${option:namespace|camel}Service could not load the created record.");
|
|
67
77
|
}
|
|
68
|
-
return record
|
|
78
|
+
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
79
|
+
action: "create",
|
|
80
|
+
options,
|
|
81
|
+
context: options?.context
|
|
82
|
+
});
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
async function updateRecord(recordId, payload = {}, options = {}) {
|
|
72
|
-
const
|
|
86
|
+
const writablePayload = await fieldAccessRuntime.enforceWritablePayload(payload, fieldAccess, {
|
|
87
|
+
action: "update",
|
|
88
|
+
recordId,
|
|
89
|
+
payload,
|
|
90
|
+
options,
|
|
91
|
+
context: options?.context
|
|
92
|
+
});
|
|
93
|
+
const record = await ${option:namespace|camel}Repository.updateById(recordId, writablePayload, options);
|
|
73
94
|
if (!record) {
|
|
74
95
|
throw new AppError(404, "Record not found.");
|
|
75
96
|
}
|
|
76
|
-
return record
|
|
97
|
+
return fieldAccessRuntime.filterReadableRecord(record, fieldAccess, {
|
|
98
|
+
action: "update",
|
|
99
|
+
recordId,
|
|
100
|
+
options,
|
|
101
|
+
context: options?.context
|
|
102
|
+
});
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
async function deleteRecord(recordId, options = {}) {
|
|
@@ -93,4 +119,19 @@ function createService({ ${option:namespace|camel}Repository } = {}) {
|
|
|
93
119
|
});
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
// Optional event override example:
|
|
123
|
+
// const serviceEvents = {
|
|
124
|
+
// ...baseServiceEvents,
|
|
125
|
+
// createRecord: [
|
|
126
|
+
// ...baseServiceEvents.createRecord,
|
|
127
|
+
// {
|
|
128
|
+
// type: "${option:namespace|snake}.custom",
|
|
129
|
+
// source: "custom",
|
|
130
|
+
// entity: "record",
|
|
131
|
+
// operation: "created",
|
|
132
|
+
// entityId: ({ result }) => result?.id
|
|
133
|
+
// }
|
|
134
|
+
// ]
|
|
135
|
+
// };
|
|
136
|
+
|
|
96
137
|
export { createService, serviceEvents };
|
|
@@ -6,26 +6,13 @@ import {
|
|
|
6
6
|
} from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
__JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__
|
|
8
8
|
__JSKIT_CRUD_RESOURCE_JSON_IMPORT__
|
|
9
|
-
function normalizeRecordInput(payload = {}) {
|
|
10
|
-
const source = normalizeObjectInput(payload);
|
|
11
|
-
const normalized = {};
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return normalized;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeRecordOutput(payload = {}) {
|
|
19
|
-
const source = normalizeObjectInput(payload);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__
|
|
23
|
-
};
|
|
24
|
-
}
|
|
10
|
+
const RESOURCE_LOOKUP_CONTAINER_KEY = "lookups";
|
|
25
11
|
|
|
26
12
|
const recordOutputSchema = Type.Object(
|
|
27
13
|
{
|
|
28
14
|
__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__
|
|
15
|
+
[RESOURCE_LOOKUP_CONTAINER_KEY]: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
29
16
|
},
|
|
30
17
|
{ additionalProperties: false }
|
|
31
18
|
);
|
|
@@ -46,11 +33,62 @@ const patchBodySchema = Type.Partial(createBodySchema, {
|
|
|
46
33
|
|
|
47
34
|
const recordOutputValidator = Object.freeze({
|
|
48
35
|
schema: recordOutputSchema,
|
|
49
|
-
normalize
|
|
36
|
+
normalize(payload = {}) {
|
|
37
|
+
const source = normalizeObjectInput(payload);
|
|
38
|
+
const normalized = {
|
|
39
|
+
__JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__
|
|
40
|
+
};
|
|
41
|
+
if (Object.hasOwn(source, RESOURCE_LOOKUP_CONTAINER_KEY)) {
|
|
42
|
+
normalized[RESOURCE_LOOKUP_CONTAINER_KEY] = source[RESOURCE_LOOKUP_CONTAINER_KEY];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
50
47
|
});
|
|
51
48
|
|
|
52
|
-
const
|
|
49
|
+
const listOutputValidator = createCursorListValidator(recordOutputValidator);
|
|
50
|
+
|
|
51
|
+
const createBodyValidator = Object.freeze({
|
|
52
|
+
schema: createBodySchema,
|
|
53
|
+
normalize(payload = {}) {
|
|
54
|
+
const source = normalizeObjectInput(payload);
|
|
55
|
+
const normalized = {};
|
|
56
|
+
|
|
57
|
+
__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__
|
|
58
|
+
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const patchBodyValidator = Object.freeze({
|
|
64
|
+
schema: patchBodySchema,
|
|
65
|
+
normalize: createBodyValidator.normalize
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const deleteOutputValidator = Object.freeze({
|
|
69
|
+
schema: Type.Object(
|
|
70
|
+
{
|
|
71
|
+
id: Type.Integer({ minimum: 1 }),
|
|
72
|
+
deleted: Type.Literal(true)
|
|
73
|
+
},
|
|
74
|
+
{ additionalProperties: false }
|
|
75
|
+
),
|
|
76
|
+
normalize(payload = {}) {
|
|
77
|
+
const source = normalizeObjectInput(payload);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: Number(source.id),
|
|
81
|
+
deleted: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const RESOURCE_FIELD_META = [];
|
|
87
|
+
|
|
88
|
+
const resource = {
|
|
53
89
|
resource: "${option:namespace|snake}",
|
|
90
|
+
tableName: __JSKIT_CRUD_TABLE_NAME__,
|
|
91
|
+
idColumn: __JSKIT_CRUD_ID_COLUMN__,
|
|
54
92
|
messages: {
|
|
55
93
|
validation: "Fix invalid values and try again.",
|
|
56
94
|
saveSuccess: "Record saved.",
|
|
@@ -58,10 +96,20 @@ const ${option:namespace|singular|camel}Resource = {
|
|
|
58
96
|
deleteSuccess: "Record deleted.",
|
|
59
97
|
deleteError: "Unable to delete record."
|
|
60
98
|
},
|
|
99
|
+
contract: {
|
|
100
|
+
lookup: {
|
|
101
|
+
containerKey: RESOURCE_LOOKUP_CONTAINER_KEY,
|
|
102
|
+
defaultInclude: "*", // Set "none" to disable lookup hydration unless include=... is passed.
|
|
103
|
+
maxDepth: 3 // Lower this to limit nested lookup hydration depth.
|
|
104
|
+
}
|
|
105
|
+
},
|
|
61
106
|
operations: {
|
|
62
107
|
list: {
|
|
108
|
+
realtime: {
|
|
109
|
+
events: ["${option:namespace|snake}.record.changed"] // Add more events e.g. for lookup records
|
|
110
|
+
},
|
|
63
111
|
method: "GET",
|
|
64
|
-
outputValidator:
|
|
112
|
+
outputValidator: listOutputValidator
|
|
65
113
|
},
|
|
66
114
|
view: {
|
|
67
115
|
method: "GET",
|
|
@@ -69,41 +117,25 @@ const ${option:namespace|singular|camel}Resource = {
|
|
|
69
117
|
},
|
|
70
118
|
create: {
|
|
71
119
|
method: "POST",
|
|
72
|
-
bodyValidator:
|
|
73
|
-
schema: createBodySchema,
|
|
74
|
-
normalize: normalizeRecordInput
|
|
75
|
-
},
|
|
120
|
+
bodyValidator: createBodyValidator,
|
|
76
121
|
outputValidator: recordOutputValidator
|
|
77
122
|
},
|
|
78
123
|
patch: {
|
|
79
124
|
method: "PATCH",
|
|
80
|
-
bodyValidator:
|
|
81
|
-
schema: patchBodySchema,
|
|
82
|
-
normalize: normalizeRecordInput
|
|
83
|
-
},
|
|
125
|
+
bodyValidator: patchBodyValidator,
|
|
84
126
|
outputValidator: recordOutputValidator
|
|
85
127
|
},
|
|
86
128
|
delete: {
|
|
87
129
|
method: "DELETE",
|
|
88
|
-
outputValidator:
|
|
89
|
-
schema: Type.Object(
|
|
90
|
-
{
|
|
91
|
-
id: Type.Integer({ minimum: 1 }),
|
|
92
|
-
deleted: Type.Literal(true)
|
|
93
|
-
},
|
|
94
|
-
{ additionalProperties: false }
|
|
95
|
-
),
|
|
96
|
-
normalize(payload = {}) {
|
|
97
|
-
const source = normalizeObjectInput(payload);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
id: Number(source.id),
|
|
101
|
-
deleted: true
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
130
|
+
outputValidator: deleteOutputValidator
|
|
105
131
|
}
|
|
106
|
-
}
|
|
132
|
+
},
|
|
133
|
+
fieldMeta: RESOURCE_FIELD_META
|
|
107
134
|
};
|
|
108
135
|
|
|
109
|
-
export {
|
|
136
|
+
export { resource };
|
|
137
|
+
|
|
138
|
+
// @jskit-contract crud.resource.field-meta.${option:namespace|snake}.v1
|
|
139
|
+
void RESOURCE_FIELD_META;
|
|
140
|
+
|
|
141
|
+
__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__
|