Skip to content

Commit

Permalink
Merge pull request #89 from almost-full-stack/acls
Browse files Browse the repository at this point in the history
Acls
  • Loading branch information
alirizwan committed May 18, 2023
2 parents 116997b + e246ce9 commit 4a2cce0
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 365 deletions.
57 changes: 57 additions & 0 deletions examples/sequelize-graphql-schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,63 @@ const { generateSchema } = require('../src/index')({
types: {
customGlobalType: { id: 'id', key: 'string', value: 'string' },
},
permissions: () => {
return Promise.resolve({
rules: {
fetch: [
{
model: 'Product',
fields: ['id', 'name'],
associations: ['Media'],
enable: true,
conditions: [
{ field: 'isActive', value: true }
],
count: false,
findOne: false
},
{
model: 'ProductMedia',
fields: ['imageId']
},
],
create: [
{
model: 'Product',
set: {
field: 'isActive', value: true
}
},
{
model: 'ProductMedia',
enable: false
},
],
update: [
{
model: 'Product',
fields: ['name'],
associations: ['Media'],
conditions: [
{ field: 'isActive', value: true }
],
},
],
delete: [
{
model: 'Product',
conditions: [
{ field: 'isActive', value: false }
],
},
{
model: 'ProductMedia',
enable: false
},
],
},
});
},
queries: {
customGlobalQuery: {
input: 'customGlobalType',
Expand Down
Binary file added examples/tests/database.db
Binary file not shown.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@
"cls-hooked": "^4.2.2",
"dataloader-sequelize": "^2.3.3",
"graphql-relay": "^0.6.0",
"graphql-request": "^3.4.0",
"graphql-sequelize": "^9.4.3",
"graphql-sequelize": "^9.5.1",
"uuid": "^8.3.2"
},
"devDependencies": {
Expand Down Expand Up @@ -82,5 +81,7 @@
"verbose": true
},
"husky": {
"hooks": {
}
}
}
140 changes: 91 additions & 49 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ const defaultOptions = {
get: '',
bulk: 'Bulk',
count: 'Count',
default: 'Default'
}
default: 'Default',
},
},

// default limit to be applied on find queries
limits: {
default: 50, // default limit. use 0 for no limit
max: 100, // maximum allowed limit. use 0 for unlimited
nested: false // whether to apply these limits on nested/sub types or not
nested: false, // whether to apply these limits on nested/sub types or not
},

// nested objects can be passed and will be mutated automatically. Only hasMany and belongsTo relation supported
Expand All @@ -50,7 +50,7 @@ const defaultOptions = {
queries: [],
mutations: [],
// instead of not generating queries/mutations this will instead throw an error.
throw: false // string message
throw: false, // string message
},
// these models will be excluded from graphql schema
exclude: [],
Expand All @@ -71,27 +71,12 @@ const defaultOptions = {
// global hooks, behaves same way as model before/extend
globalHooks: {
before: {}, // will be executed before all auto-generated mutations/queries (fetch/create/update/destroy)
extend: {} // will be executed after all auto-generated mutations/queries (fetch/create/update/destroy)
extend: {}, // will be executed after all auto-generated mutations/queries (fetch/create/update/destroy)
},
findOneQueries: false, // create a find one query for each model (i.e. ProductByPk), which takes primary key (i.e. id) as argument and returns one item. Can also pass an array of models to create for specific models only (i.e. ['Product', 'Image'])
fetchDeleted: false, // Globally when using queries, this will allow to fetch both deleted and undeleted records (works only when tables have paranoid option enabled)
restoreDeleted: false, // Applies globally, create restore endpoint for deleted records
noDefaults: true, // set it to false to generate empty default queries
/**
* {
* strict: true | false // everything restricted except specified rules
* applySchema: true | false // schema generated reflects ommitted fields and queries/mutations instead of throwing error
* rules: {
* [MODEL_NAME0]: '*' // no restrictions
* [MODEL_NAME1]: { fetch: ['name'], update: ['name'], delete: false, create: true } // only exposes name field from model1, only able to update name field, cannot delete but can create new record
* [MODEL_NAME2]: { fetch: ['-name'], update: ['-name'], delete: true, create: false } // excludes name from model2 but includes everything else same for update, can delete but cannot create new
* [MODEL_NAME3]: { fetch: ['-name, id'], update: '*', delete: false, create: false } // excludes name and only includes id from model2, can update all fields, cannot delete or create
* [MUTATION_NAME]: true // allowed to call this mutation
* [QUERY_NAME]: false // not allwoed to call this query
* }
* }
*/

/**
*
* rules: {
Expand All @@ -110,14 +95,14 @@ const defaultOptions = {
return Promise.resolve();
},
// executes before all queries/mutations
authorizer() {
authorizer(src, arg, ctx) {
return Promise.resolve();
},
// executes when exceptions are thrown
errorHandler: {
'ETIMEDOUT': { statusCode: 503 }
ETIMEDOUT: { statusCode: 503 },
},
debug: false
debug: false,
};

// Model options model.graphql
Expand All @@ -129,11 +114,12 @@ const defaultModelGraphqlOptions = {
// scope usage is highy recommended.
scopes: null, // common scope to be applied on all find/update/destroy operations
alias: {}, // rename default queries/mutations to specified custom name
bulk: { // OR bulk: ['create', 'destroy', ....]
bulk: {
// OR bulk: ['create', 'destroy', ....]
enabled: [], // enable bulk options ['create', 'destroy', 'update']
// Use bulkColumn when using bulk option for 'create' when using returning true and to increase efficiency
bulkColumn: false, // bulk identifier column, when bulk creating this column will be auto filled with a uuid and later used to fetch added records 'columnName' or ['columnName', true] when using a foreign key as bulk column
returning: true // This will return all created/updated items, doesn't use sequelize returning option
returning: true, // This will return all created/updated items, doesn't use sequelize returning option
},
types: {}, // user defined custom types: type names should be unique throughout the project
mutations: {}, // user defined custom mutations: : mutation names should be unique throughout the project
Expand All @@ -147,16 +133,14 @@ const defaultModelGraphqlOptions = {
readonly: false, // exclude create/delete/update mutations automatically
fetchDeleted: false, // same as fetchDeleted as global except it lets you override global settings
restoreDeleted: false, // same as restoreDeleted as global except it lets you override global settings
find: {} // define graphql-sequelize find hooks {before, after}
find: {}, // define graphql-sequelize find hooks {before, after}
};

const GenerateQueries = require('./libs/generateQueries');
const GenerateMutations = require('./libs/generateMutations');
const GenerateTypes = require('./libs/generateTypes');
const errorHandler = (options) => {

return (error) => {

for (const name in options.errorHandler) {
if (error.message.indexOf(name) > -1) {
Object.assign(error, options.errorHandler[name]);
Expand All @@ -165,16 +149,16 @@ const errorHandler = (options) => {
}

return error;

};

};

function generateSchema(options) {

return (models, context) => {
return async (models, context) => {
assert(models.Sequelize, 'Sequelize not found as models.Sequelize.');
assert(models.sequelize, 'sequelize instance not found as models.sequelize.');
assert(
models.sequelize,
'sequelize instance not found as models.sequelize.'
);

if (options.dataloader) {
options.dataloaderContext = createContext(models.sequelize);
Expand All @@ -184,6 +168,13 @@ function generateSchema(options) {
options.sequelize = models.sequelize;
options.models = models;

const generatedPermissions = await options.permissions({
models,
...context,
});

options.GC_PERMISSIONS = { strict: true, ...generatedPermissions };

options.Sequelize.useCLS(cls.createNamespace(TRANSACTION_NAMESPACE));

const { generateModelTypes } = GenerateTypes(options);
Expand All @@ -192,44 +183,78 @@ function generateSchema(options) {
const modelsIncluded = {};

for (const modelName in models) {

const model = models[modelName];

if ('name' in model && modelName !== 'Sequelize' && !options.exclude.includes(modelName)) {
if (
'name' in model &&
modelName !== 'Sequelize' &&
!options.exclude.includes(modelName)
) {
model.graphql = model.graphql || defaultModelGraphqlOptions;
model.graphql.attributes = Object.assign({}, defaultModelGraphqlOptions.attributes, model.graphql.attributes);
model.graphql = Object.assign({}, defaultModelGraphqlOptions, model.graphql);
model.graphql.attributes = Object.assign(
{},
defaultModelGraphqlOptions.attributes,
model.graphql.attributes
);
model.graphql = Object.assign(
{},
defaultModelGraphqlOptions,
model.graphql
);
modelsIncluded[modelName] = model;
}

}

const modelTypes = generateModelTypes(modelsIncluded, {}, options);

return Promise.resolve({
query: generateQueries(modelsIncluded, modelTypes.outputTypes, modelTypes.inputTypes),
mutation: generateMutations(modelsIncluded, modelTypes.outputTypes, modelTypes.inputTypes)
query: generateQueries(
modelsIncluded,
modelTypes.outputTypes,
modelTypes.inputTypes
),
mutation: generateMutations(
modelsIncluded,
modelTypes.outputTypes,
modelTypes.inputTypes
),
});
};


}

const init = (_options) => {

const newOptions = { ..._options };

newOptions.naming = Object.assign({}, defaultOptions.naming, newOptions.naming);
newOptions.naming.type = Object.assign({}, defaultOptions.naming.type, newOptions.naming.type);
newOptions.exposeOnly = Object.assign({}, defaultOptions.exposeOnly, newOptions.exposeOnly);
newOptions.limits = Object.assign({}, defaultOptions.limits, newOptions.limits);
newOptions.naming = Object.assign(
{},
defaultOptions.naming,
newOptions.naming
);
newOptions.naming.type = Object.assign(
{},
defaultOptions.naming.type,
newOptions.naming.type
);
newOptions.exposeOnly = Object.assign(
{},
defaultOptions.exposeOnly,
newOptions.exposeOnly
);
newOptions.limits = Object.assign(
{},
defaultOptions.limits,
newOptions.limits
);

const options = Object.assign({}, defaultOptions, newOptions);

options.dataloaderContext = null;

const resetCache = () => {
if (options.dataloaderContext && options.dataloaderContext.loaders.autogenerated) {
if (
options.dataloaderContext &&
options.dataloaderContext.loaders.autogenerated
) {
options.dataloaderContext.loaders.autogenerated.reset();
}
};
Expand All @@ -240,7 +265,7 @@ const init = (_options) => {
resetCache,
// use this to prime custom queries
dataloaderContext: options.dataloaderContext,
errorHandler: errorHandler(options)
errorHandler: errorHandler(options),
};
};

Expand All @@ -251,3 +276,20 @@ const init = (_options) => {
init.define = define;

module.exports = init;

/*
rules: {
fetch: {
resources: [
{
model: "Job",
fields: ["id", "number"],
conditions: [
{ field: "userId", value: ":ctx.user.id" },
{ field: "status", value: true },
],
},
];
}
}
*/
Loading

0 comments on commit 4a2cce0

Please sign in to comment.