@directus/api 23.0.0 → 23.1.0

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 (42) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/controllers/activity.js +30 -27
  3. package/dist/controllers/assets.js +1 -1
  4. package/dist/controllers/comments.d.ts +2 -0
  5. package/dist/controllers/comments.js +153 -0
  6. package/dist/controllers/versions.js +10 -5
  7. package/dist/database/index.js +3 -0
  8. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  9. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  10. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  11. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  12. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  13. package/dist/database/run-ast/lib/get-db-query.js +2 -2
  14. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  15. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  16. package/dist/database/run-ast/run-ast.js +8 -1
  17. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  18. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  19. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  20. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  21. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  22. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  23. package/dist/services/activity.d.ts +1 -7
  24. package/dist/services/activity.js +0 -103
  25. package/dist/services/assets.js +5 -4
  26. package/dist/services/collections.js +6 -4
  27. package/dist/services/comments.d.ts +31 -0
  28. package/dist/services/comments.js +374 -0
  29. package/dist/services/graphql/index.js +17 -16
  30. package/dist/services/index.d.ts +1 -0
  31. package/dist/services/index.js +1 -0
  32. package/dist/services/items.js +3 -1
  33. package/dist/services/mail/index.d.ts +2 -1
  34. package/dist/services/mail/index.js +4 -1
  35. package/dist/services/payload.js +15 -14
  36. package/dist/services/users.js +1 -0
  37. package/dist/services/versions.js +59 -44
  38. package/dist/utils/apply-diff.js +5 -6
  39. package/dist/utils/get-service.js +3 -1
  40. package/dist/utils/sanitize-schema.d.ts +1 -1
  41. package/dist/utils/sanitize-schema.js +2 -0
  42. package/package.json +51 -51
package/dist/app.js CHANGED
@@ -14,6 +14,7 @@ import accessRouter from './controllers/access.js';
14
14
  import assetsRouter from './controllers/assets.js';
15
15
  import authRouter from './controllers/auth.js';
16
16
  import collectionsRouter from './controllers/collections.js';
17
+ import commentsRouter from './controllers/comments.js';
17
18
  import dashboardsRouter from './controllers/dashboards.js';
18
19
  import extensionsRouter from './controllers/extensions.js';
19
20
  import fieldsRouter from './controllers/fields.js';
@@ -209,6 +210,7 @@ export default async function createApp() {
209
210
  app.use('/access', accessRouter);
210
211
  app.use('/assets', assetsRouter);
211
212
  app.use('/collections', collectionsRouter);
213
+ app.use('/comments', commentsRouter);
212
214
  app.use('/dashboards', dashboardsRouter);
213
215
  app.use('/extensions', extensionsRouter);
214
216
  app.use('/fields', fieldsRouter);
@@ -1,15 +1,13 @@
1
- import { Action } from '@directus/constants';
2
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
2
  import express from 'express';
4
3
  import Joi from 'joi';
5
- import { ErrorCode, ForbiddenError, InvalidPayloadError } from '@directus/errors';
6
4
  import { respond } from '../middleware/respond.js';
7
5
  import useCollection from '../middleware/use-collection.js';
8
6
  import { validateBatch } from '../middleware/validate-batch.js';
9
7
  import { ActivityService } from '../services/activity.js';
8
+ import { CommentsService } from '../services/comments.js';
10
9
  import { MetaService } from '../services/meta.js';
11
10
  import asyncHandler from '../utils/async-handler.js';
12
- import { getIPFromReq } from '../utils/get-ip-from-req.js';
13
11
  const router = express.Router();
14
12
  router.use(useCollection('directus_activity'));
15
13
  const readHandler = asyncHandler(async (req, res, next) => {
@@ -22,6 +20,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
22
20
  schema: req.schema,
23
21
  });
24
22
  let result;
23
+ let isComment;
25
24
  if (req.singleton) {
26
25
  result = await service.readSingleton(req.sanitizedQuery);
27
26
  }
@@ -29,9 +28,24 @@ const readHandler = asyncHandler(async (req, res, next) => {
29
28
  result = await service.readMany(req.body.keys, req.sanitizedQuery);
30
29
  }
31
30
  else {
32
- result = await service.readByQuery(req.sanitizedQuery);
31
+ const sanitizedFilter = req.sanitizedQuery.filter;
32
+ if (sanitizedFilter &&
33
+ '_and' in sanitizedFilter &&
34
+ Array.isArray(sanitizedFilter['_and']) &&
35
+ sanitizedFilter['_and'].find((andItem) => 'action' in andItem && '_eq' in andItem['action'] && andItem['action']['_eq'] === 'comment')) {
36
+ const commentsService = new CommentsService({
37
+ accountability: req.accountability,
38
+ schema: req.schema,
39
+ serviceOrigin: 'activity',
40
+ });
41
+ result = await commentsService.readByQuery(req.sanitizedQuery);
42
+ isComment = true;
43
+ }
44
+ else {
45
+ result = await service.readByQuery(req.sanitizedQuery);
46
+ }
33
47
  }
34
- const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
48
+ const meta = await metaService.getMetaForQuery(isComment ? 'directus_comments' : 'directus_activity', req.sanitizedQuery);
35
49
  res.locals['payload'] = {
36
50
  data: result,
37
51
  meta,
@@ -57,22 +71,16 @@ const createCommentSchema = Joi.object({
57
71
  item: [Joi.number().required(), Joi.string().required()],
58
72
  });
59
73
  router.post('/comment', asyncHandler(async (req, res, next) => {
60
- const service = new ActivityService({
74
+ const service = new CommentsService({
61
75
  accountability: req.accountability,
62
76
  schema: req.schema,
77
+ serviceOrigin: 'activity',
63
78
  });
64
79
  const { error } = createCommentSchema.validate(req.body);
65
80
  if (error) {
66
81
  throw new InvalidPayloadError({ reason: error.message });
67
82
  }
68
- const primaryKey = await service.createOne({
69
- ...req.body,
70
- action: Action.COMMENT,
71
- user: req.accountability?.user,
72
- ip: getIPFromReq(req),
73
- user_agent: req.accountability?.userAgent,
74
- origin: req.get('origin'),
75
- });
83
+ const primaryKey = await service.createOne(req.body);
76
84
  try {
77
85
  const record = await service.readOne(primaryKey, req.sanitizedQuery);
78
86
  res.locals['payload'] = {
@@ -91,17 +99,18 @@ const updateCommentSchema = Joi.object({
91
99
  comment: Joi.string().required(),
92
100
  });
93
101
  router.patch('/comment/:pk', asyncHandler(async (req, res, next) => {
94
- const service = new ActivityService({
102
+ const commentsService = new CommentsService({
95
103
  accountability: req.accountability,
96
104
  schema: req.schema,
105
+ serviceOrigin: 'activity',
97
106
  });
98
107
  const { error } = updateCommentSchema.validate(req.body);
99
108
  if (error) {
100
109
  throw new InvalidPayloadError({ reason: error.message });
101
110
  }
102
- const primaryKey = await service.updateOne(req.params['pk'], req.body);
111
+ const primaryKey = await commentsService.updateOne(req.params['pk'], req.body);
103
112
  try {
104
- const record = await service.readOne(primaryKey, req.sanitizedQuery);
113
+ const record = await commentsService.readOne(primaryKey, req.sanitizedQuery);
105
114
  res.locals['payload'] = {
106
115
  data: record || null,
107
116
  };
@@ -115,18 +124,12 @@ router.patch('/comment/:pk', asyncHandler(async (req, res, next) => {
115
124
  return next();
116
125
  }), respond);
117
126
  router.delete('/comment/:pk', asyncHandler(async (req, _res, next) => {
118
- const service = new ActivityService({
127
+ const commentsService = new CommentsService({
119
128
  accountability: req.accountability,
120
129
  schema: req.schema,
130
+ serviceOrigin: 'activity',
121
131
  });
122
- const adminService = new ActivityService({
123
- schema: req.schema,
124
- });
125
- const item = await adminService.readOne(req.params['pk'], { fields: ['action'] });
126
- if (!item || item['action'] !== Action.COMMENT) {
127
- throw new ForbiddenError();
128
- }
129
- await service.deleteOne(req.params['pk']);
132
+ await commentsService.deleteOne(req.params['pk']);
130
133
  return next();
131
134
  }), respond);
132
135
  export default router;
@@ -104,7 +104,7 @@ asyncHandler(async (req, res, next) => {
104
104
  return helmet.contentSecurityPolicy(merge({
105
105
  useDefaults: false,
106
106
  directives: {
107
- defaultSrc: ['none'],
107
+ defaultSrc: [`'none'`],
108
108
  },
109
109
  }, getConfigFromEnv('ASSETS_CONTENT_SECURITY_POLICY')))(req, res, next);
110
110
  }),
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,153 @@
1
+ import { ErrorCode, isDirectusError } from '@directus/errors';
2
+ import express from 'express';
3
+ import { respond } from '../middleware/respond.js';
4
+ import useCollection from '../middleware/use-collection.js';
5
+ import { validateBatch } from '../middleware/validate-batch.js';
6
+ import { CommentsService } from '../services/comments.js';
7
+ import { MetaService } from '../services/meta.js';
8
+ import asyncHandler from '../utils/async-handler.js';
9
+ import { sanitizeQuery } from '../utils/sanitize-query.js';
10
+ const router = express.Router();
11
+ router.use(useCollection('directus_comments'));
12
+ router.post('/', asyncHandler(async (req, res, next) => {
13
+ const service = new CommentsService({
14
+ accountability: req.accountability,
15
+ schema: req.schema,
16
+ serviceOrigin: 'comments',
17
+ });
18
+ const savedKeys = [];
19
+ if (Array.isArray(req.body)) {
20
+ const keys = await service.createMany(req.body);
21
+ savedKeys.push(...keys);
22
+ }
23
+ else {
24
+ const key = await service.createOne(req.body);
25
+ savedKeys.push(key);
26
+ }
27
+ try {
28
+ if (Array.isArray(req.body)) {
29
+ const records = await service.readMany(savedKeys, req.sanitizedQuery);
30
+ res.locals['payload'] = { data: records };
31
+ }
32
+ else {
33
+ const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
34
+ res.locals['payload'] = { data: record };
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
39
+ return next();
40
+ }
41
+ throw error;
42
+ }
43
+ return next();
44
+ }), respond);
45
+ const readHandler = asyncHandler(async (req, res, next) => {
46
+ const service = new CommentsService({
47
+ accountability: req.accountability,
48
+ schema: req.schema,
49
+ serviceOrigin: 'comments',
50
+ });
51
+ const metaService = new MetaService({
52
+ accountability: req.accountability,
53
+ schema: req.schema,
54
+ });
55
+ let result;
56
+ if (req.body.keys) {
57
+ result = await service.readMany(req.body.keys, req.sanitizedQuery);
58
+ }
59
+ else {
60
+ result = await service.readByQuery(req.sanitizedQuery);
61
+ }
62
+ const meta = await metaService.getMetaForQuery('directus_comments', req.sanitizedQuery);
63
+ res.locals['payload'] = { data: result, meta };
64
+ return next();
65
+ });
66
+ router.get('/', validateBatch('read'), readHandler, respond);
67
+ router.search('/', validateBatch('read'), readHandler, respond);
68
+ router.get('/:pk', asyncHandler(async (req, res, next) => {
69
+ const service = new CommentsService({
70
+ accountability: req.accountability,
71
+ schema: req.schema,
72
+ serviceOrigin: 'comments',
73
+ });
74
+ const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
75
+ res.locals['payload'] = { data: record || null };
76
+ return next();
77
+ }), respond);
78
+ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
79
+ const service = new CommentsService({
80
+ accountability: req.accountability,
81
+ schema: req.schema,
82
+ serviceOrigin: 'comments',
83
+ });
84
+ let keys = [];
85
+ if (Array.isArray(req.body)) {
86
+ keys = await service.updateBatch(req.body);
87
+ }
88
+ else if (req.body.keys) {
89
+ keys = await service.updateMany(req.body.keys, req.body.data);
90
+ }
91
+ else {
92
+ const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
93
+ keys = await service.updateByQuery(sanitizedQuery, req.body.data);
94
+ }
95
+ try {
96
+ const result = await service.readMany(keys, req.sanitizedQuery);
97
+ res.locals['payload'] = { data: result };
98
+ }
99
+ catch (error) {
100
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
101
+ return next();
102
+ }
103
+ throw error;
104
+ }
105
+ return next();
106
+ }), respond);
107
+ router.patch('/:pk', asyncHandler(async (req, res, next) => {
108
+ const service = new CommentsService({
109
+ accountability: req.accountability,
110
+ schema: req.schema,
111
+ serviceOrigin: 'comments',
112
+ });
113
+ const primaryKey = await service.updateOne(req.params['pk'], req.body);
114
+ try {
115
+ const record = await service.readOne(primaryKey, req.sanitizedQuery);
116
+ res.locals['payload'] = { data: record };
117
+ }
118
+ catch (error) {
119
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
120
+ return next();
121
+ }
122
+ throw error;
123
+ }
124
+ return next();
125
+ }), respond);
126
+ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next) => {
127
+ const service = new CommentsService({
128
+ accountability: req.accountability,
129
+ schema: req.schema,
130
+ serviceOrigin: 'comments',
131
+ });
132
+ if (Array.isArray(req.body)) {
133
+ await service.deleteMany(req.body);
134
+ }
135
+ else if (req.body.keys) {
136
+ await service.deleteMany(req.body.keys);
137
+ }
138
+ else {
139
+ const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
140
+ await service.deleteByQuery(sanitizedQuery);
141
+ }
142
+ return next();
143
+ }), respond);
144
+ router.delete('/:pk', asyncHandler(async (req, _res, next) => {
145
+ const service = new CommentsService({
146
+ accountability: req.accountability,
147
+ schema: req.schema,
148
+ serviceOrigin: 'comments',
149
+ });
150
+ await service.deleteOne(req.params['pk']);
151
+ return next();
152
+ }), respond);
153
+ export default router;
@@ -154,8 +154,14 @@ router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
154
154
  });
155
155
  const version = await service.readOne(req.params['pk']);
156
156
  const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);
157
- const saves = await service.getVersionSavesById(version['id']);
158
- const current = assign({}, ...saves);
157
+ let current;
158
+ if (version['delta']) {
159
+ current = version['delta'];
160
+ }
161
+ else {
162
+ const saves = await service.getVersionSavesById(version['id']);
163
+ current = assign({}, ...saves);
164
+ }
159
165
  const main = await service.getMainItem(version['collection'], version['item']);
160
166
  res.locals['payload'] = { data: { outdated, mainHash, current, main } };
161
167
  return next();
@@ -167,9 +173,8 @@ router.post('/:pk/save', asyncHandler(async (req, res, next) => {
167
173
  });
168
174
  const version = await service.readOne(req.params['pk']);
169
175
  const mainItem = await service.getMainItem(version['collection'], version['item']);
170
- await service.save(req.params['pk'], req.body);
171
- const saves = await service.getVersionSavesById(req.params['pk']);
172
- const result = assign(mainItem, ...saves);
176
+ const updatedVersion = await service.save(req.params['pk'], req.body);
177
+ const result = assign(mainItem, updatedVersion);
173
178
  res.locals['payload'] = { data: result || null };
174
179
  return next();
175
180
  }), respond);
@@ -140,6 +140,9 @@ export function getDatabase() {
140
140
  times.delete(queryInfo.__knexUid);
141
141
  }
142
142
  logger.trace(`[${delta ? delta.toFixed(3) : '?'}ms] ${queryInfo.sql} [${(queryInfo.bindings ?? []).join(', ')}]`);
143
+ })
144
+ .on('query-error', (_, queryInfo) => {
145
+ times.delete(queryInfo.__knexUid);
143
146
  });
144
147
  return database;
145
148
  }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { Action } from '@directus/constants';
2
+ export async function up(knex) {
3
+ await knex.schema.createTable('directus_comments', (table) => {
4
+ table.uuid('id').primary().notNullable();
5
+ table
6
+ .string('collection', 64)
7
+ .notNullable()
8
+ .references('collection')
9
+ .inTable('directus_collections')
10
+ .onDelete('CASCADE');
11
+ table.string('item').notNullable();
12
+ table.text('comment').notNullable();
13
+ table.timestamp('date_created').defaultTo(knex.fn.now());
14
+ table.timestamp('date_updated').defaultTo(knex.fn.now());
15
+ table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
16
+ // Cannot have two constraints from/to the same table, handled on API side
17
+ table.uuid('user_updated').references('id').inTable('directus_users');
18
+ });
19
+ }
20
+ export async function down(knex) {
21
+ const rowsLimit = 50;
22
+ let hasMore = true;
23
+ while (hasMore) {
24
+ const comments = await knex
25
+ .select('id', 'collection', 'item', 'comment', 'date_created', 'user_created')
26
+ .from('directus_comments')
27
+ .limit(rowsLimit);
28
+ if (comments.length === 0) {
29
+ hasMore = false;
30
+ break;
31
+ }
32
+ await knex.transaction(async (trx) => {
33
+ for (const comment of comments) {
34
+ const migratedRecords = await trx('directus_activity')
35
+ .select('id')
36
+ .where('collection', '=', 'directus_comments')
37
+ .andWhere('item', '=', comment.id)
38
+ .andWhere('action', '=', Action.CREATE)
39
+ .limit(1);
40
+ if (migratedRecords[0]) {
41
+ await trx('directus_activity')
42
+ .update({
43
+ action: Action.COMMENT,
44
+ collection: comment.collection,
45
+ item: comment.item,
46
+ comment: comment.comment,
47
+ })
48
+ .where('id', '=', migratedRecords[0].id);
49
+ }
50
+ else {
51
+ await trx('directus_activity').insert({
52
+ action: Action.COMMENT,
53
+ collection: comment.collection,
54
+ item: comment.item,
55
+ comment: comment.comment,
56
+ user: comment.user_created,
57
+ timestamp: comment.date_created,
58
+ });
59
+ }
60
+ await trx('directus_comments').where('id', '=', comment.id).delete();
61
+ }
62
+ });
63
+ }
64
+ await knex.schema.dropTable('directus_comments');
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,10 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_versions', (table) => {
3
+ table.json('delta');
4
+ });
5
+ }
6
+ export async function down(knex) {
7
+ await knex.schema.alterTable('directus_versions', (table) => {
8
+ table.dropColumn('delta');
9
+ });
10
+ }
@@ -1,4 +1,14 @@
1
- import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
+ import type { Context } from '../../../permissions/types.js';
3
4
  import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
4
- export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): Knex.QueryBuilder;
5
+ export type DBQueryOptions = {
6
+ table: string;
7
+ fieldNodes: (FieldNode | FunctionFieldNode)[];
8
+ o2mNodes: O2MNode[];
9
+ query: Query;
10
+ cases: Filter[];
11
+ permissions: Permission[];
12
+ permissionsOnly?: boolean;
13
+ };
14
+ export declare function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }: DBQueryOptions, { knex, schema }: Context): Knex.QueryBuilder;
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
9
9
  import { getNodeAlias } from '../utils/get-field-alias.js';
10
10
  import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
11
11
  import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
12
- export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
12
+ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }, { knex, schema }) {
13
13
  const aliasMap = Object.create(null);
14
14
  const env = useEnv();
15
- const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
15
+ const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly);
16
16
  const queryCopy = cloneDeep(query);
17
17
  const helpers = getHelpers(knex);
18
18
  const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
@@ -0,0 +1,15 @@
1
+ import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AST } from '../../../types/ast.js';
4
+ type FetchPermittedAstRootFieldsOptions = {
5
+ schema: SchemaOverview;
6
+ accountability: Accountability;
7
+ knex: Knex;
8
+ action: PermissionsAction;
9
+ };
10
+ /**
11
+ * Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
12
+ * same way as `runAst` but only returns flags (1/null) instead of actual field values.
13
+ */
14
+ export declare function fetchPermittedAstRootFields(originalAST: AST, { schema, accountability, knex, action }: FetchPermittedAstRootFieldsOptions): Promise<any>;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
4
+ import { getDBQuery } from '../lib/get-db-query.js';
5
+ import { parseCurrentLevel } from '../lib/parse-current-level.js';
6
+ /**
7
+ * Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
8
+ * same way as `runAst` but only returns flags (1/null) instead of actual field values.
9
+ */
10
+ export async function fetchPermittedAstRootFields(originalAST, { schema, accountability, knex, action }) {
11
+ const ast = cloneDeep(originalAST);
12
+ const { name: collection, children, cases, query } = ast;
13
+ // Retrieve the database columns to select in the current AST
14
+ const { fieldNodes } = await parseCurrentLevel(schema, collection, children, query);
15
+ let permissions = [];
16
+ if (accountability && !accountability.admin) {
17
+ const policies = await fetchPolicies(accountability, { schema, knex });
18
+ permissions = await fetchPermissions({ action, accountability, policies }, { schema, knex });
19
+ }
20
+ return getDBQuery({
21
+ table: collection,
22
+ fieldNodes,
23
+ o2mNodes: [],
24
+ query,
25
+ cases,
26
+ permissions,
27
+ permissionsOnly: true,
28
+ }, { schema, knex });
29
+ }
@@ -36,7 +36,14 @@ export async function runAst(originalAST, schema, accountability, options) {
36
36
  permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
37
37
  }
38
38
  // The actual knex query builder instance. This is a promise that resolves with the raw items from the db
39
- const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
39
+ const dbQuery = getDBQuery({
40
+ table: collection,
41
+ fieldNodes,
42
+ o2mNodes,
43
+ query,
44
+ cases,
45
+ permissions,
46
+ }, { schema, knex });
40
47
  const rawItems = await dbQuery;
41
48
  if (!rawItems)
42
49
  return null;
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
6
6
  /** Don't assign an alias to the column but instead return the column as is */
7
7
  noAlias?: boolean;
8
8
  }
9
- export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
9
+ export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, permissionsOnly?: boolean): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
10
10
  export {};
@@ -4,7 +4,7 @@ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
4
4
  import { getHelpers } from '../../helpers/index.js';
5
5
  import { applyCaseWhen } from './apply-case-when.js';
6
6
  import { getNodeAlias } from './get-field-alias.js';
7
- export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
7
+ export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly) {
8
8
  const helpers = getHelpers(knex);
9
9
  return function (fieldNode, options) {
10
10
  // Don't assign an alias to the column expression if the field has a whenCase
@@ -22,7 +22,15 @@ export function getColumnPreprocessor(knex, schema, table, cases, permissions, a
22
22
  field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
23
23
  }
24
24
  let column;
25
- if (field?.type?.startsWith('geometry')) {
25
+ if (permissionsOnly) {
26
+ if (noAlias) {
27
+ column = knex.raw(1);
28
+ }
29
+ else {
30
+ column = knex.raw('1 as ??', [alias]);
31
+ }
32
+ }
33
+ else if (field?.type?.startsWith('geometry')) {
26
34
  column = helpers.st.asText(table, field.field, rawColumnAlias);
27
35
  }
28
36
  else if (fieldNode.type === 'functionField') {
@@ -5,5 +5,6 @@ export interface ValidateItemAccessOptions {
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
7
  primaryKeys: PrimaryKey[];
8
+ fields?: string[];
8
9
  }
9
- export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<boolean>;
10
+ export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<any>;
@@ -1,5 +1,4 @@
1
- import { getAstFromQuery } from '../../../../database/get-ast-from-query/get-ast-from-query.js';
2
- import { runAst } from '../../../../database/run-ast/run-ast.js';
1
+ import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
3
2
  import { processAst } from '../../process-ast/process-ast.js';
4
3
  export async function validateItemAccess(options, context) {
5
4
  const primaryKeyField = context.schema.collections[options.collection]?.primary;
@@ -8,17 +7,14 @@ export async function validateItemAccess(options, context) {
8
7
  }
9
8
  // When we're looking up access to specific items, we have to read them from the database to
10
9
  // make sure you are allowed to access them.
11
- const query = {
12
- // We don't actually need any of the field data, just want to know if we can read the item as
13
- // whole or not
14
- fields: [],
15
- limit: options.primaryKeys.length,
10
+ const ast = {
11
+ type: 'root',
12
+ name: options.collection,
13
+ query: { limit: options.primaryKeys.length },
14
+ // Act as if every field was a "normal" field
15
+ children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
16
+ cases: [],
16
17
  };
17
- const ast = await getAstFromQuery({
18
- accountability: options.accountability,
19
- query,
20
- collection: options.collection,
21
- }, context);
22
18
  await processAst({ ast, ...options }, context);
23
19
  // Inject the filter after the permissions have been processed, as to not require access to the primary key
24
20
  ast.query.filter = {
@@ -26,8 +22,17 @@ export async function validateItemAccess(options, context) {
26
22
  _in: options.primaryKeys,
27
23
  },
28
24
  };
29
- const items = await runAst(ast, context.schema, options.accountability, { knex: context.knex });
25
+ const items = await fetchPermittedAstRootFields(ast, {
26
+ schema: context.schema,
27
+ accountability: options.accountability,
28
+ knex: context.knex,
29
+ action: options.action,
30
+ });
30
31
  if (items && items.length === options.primaryKeys.length) {
32
+ const { fields } = options;
33
+ if (fields) {
34
+ return items.every((item) => fields.every((field) => item[field] === 1));
35
+ }
31
36
  return true;
32
37
  }
33
38
  return false;
@@ -5,6 +5,7 @@ export interface ValidateAccessOptions {
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
7
  primaryKeys?: PrimaryKey[];
8
+ fields?: string[];
8
9
  }
9
10
  /**
10
11
  * Validate if the current user has access to perform action against the given collection and