Giter Site home page Giter Site logo

crest's Issues

CDI integration

Hi,

Makes a few projects I'm integrating crest with CDI and goals of this issue are: 1. at least share how to do it, 2. show some limitation of crest currently (the biggest being the env threadlocal but I'm not sure how we would like to resolve it without breaking).

Integration

Personally I extend crest with 4 "points":

  • a CDI aware main to scan with CDI the commands
  • a CDI extension to make @Command a qualifier and optionally adds it on classes using @Command at method only level. All annotation methods will be set as @NonBinding
  • a CDI aware environment for service injection (command parameters)
  • a CDI target indeed to use the right bean instance

CDI Main

public class CdiCrestMain extends Main {
    private final BeanManager beanManager;
    private final DefaultsContext context;

    public CdiCrestMain(final BeanManager beanManager, final DefaultsContext context) {
        super(context);
        this.beanManager = beanManager;
        this.context = context;
        registerCommands();
    }

    public Map<String, Cmd> commands() {
        return commands;
    }

    private void registerCommands() {
        final var qualifier = new CommandQualifier();
        this.beanManager.getBeans(Object.class, qualifier)
                .forEach(bean -> commands.putAll(Commands.get(bean.getBeanClass(), new CdiTarget(beanManager, bean), context)));
    }

    public void apply(final String... args) throws Exception {
        final var environment = new CDIEnvironment(beanManager);
        final var envHolder = Environment.ENVIRONMENT_THREAD_LOCAL;
        envHolder.set(environment);
        try {
            super.main(environment, args);
        } finally {
            // crest API has a default value so we can't really store previous value and reset it
            envHolder.remove();
        }
    }

    private static class CommandQualifier extends AnnotationLiteral<Command> implements Command {
        @Override
        public String value() {
            return null;
        }

        @Override
        public String usage() {
            return null;
        }

        @Override
        public Class<?>[] interceptedBy() {
            return new Class[0];
        }
    }
}

Here see the limitatio nof the threadlocal in apply, we should be able to stack it to reset it after usage but here we can't since it has a default. An option is Environment.hasValue() which would avoid the default and Environment.value() but it would break backward compatibility so not sure the goal. I would also prefer the environment to be passed to the commands like the context and not depend on a thread local.

CDI Environment

public class CDIEnvironment extends SystemEnvironment {
    private final BeanManager beanManager;

    public CDIEnvironment(final BeanManager beanManager) {
        this.beanManager = beanManager;
    }

    @Override
    public <T> T findService(final Class<T> type) {
        return ofNullable(super.findService(type))
                .orElseGet(() -> {
                    final var bean = beanManager.resolve(beanManager.getBeans(type));
                    if (bean == null || !beanManager.isNormalScope(bean.getScope())) {
                        throw new IllegalStateException("For now only normal scoped beans can be used as command parameter injection.");
                    }
                    return type.cast(beanManager.getReference(bean, type, beanManager.createCreationalContext(null)));
                });
    }
}

We can make it supporting not normal scoped beans - not sure it is that useful, maybe, but it means storing the instances for the call duration and release the context after. here the API should be enhanced to return a CrestInstance() { T value(); void close(); } which would be automatically stored/unwrapped by the runtime.

CDI Target

public class CdiTarget implements Target {
    private final BeanManager beanManager;
    private final Bean<?> bean;

    public CdiTarget(final BeanManager beanManager, final Bean<?> bean) {
        this.beanManager = beanManager;
        this.bean = bean;
    }

    @Override
    public Object invoke(final Method method, final Object... args) throws InvocationTargetException, IllegalAccessException {
        return method.invoke(getInstance(method), args);
    }

    @Override
    public Object getInstance(final Method method) {
        final var creationalContext = beanManager.createCreationalContext(null);
        return beanManager.getReference(bean, bean.getBeanClass(), creationalContext);
    }
}

Here again we have the same limitation than for the services, CrestInstance can solve it. Theorically we should be able to have a more adapted API since getInstance is used for bean validation whereas we should get something like <T, Object> T withInstance(Function<T, Object>) API which would wrap the bean validation+method invocation to lookup a single time the instance and be able to release it after method invocation (which would need the instance to call injected in parameters for ex).
Here again, not sure in terms of backward compatibility what is desired.

CDI Extension to make @Command a qualifier

This one is not 100% required and can be replaced by an extension for the scanning but this implementation is faster (always better for a CLI) even if it has the limitation to not enable to have alternatives for commands - which is not a big limitation since it can be replaced by a small indirection if needed (never?).

public class CrestCommandQualifierExtension implements Extension {
    public void onStart(@Observes final BeforeBeanDiscovery beforeBeanDiscovery,
                        final BeanManager beanManager) {
        beforeBeanDiscovery.addQualifier(new NonBindingType<>(beanManager.createAnnotatedType(Command.class)));
    }

    private static class NonBindingType<T> implements AnnotatedType<T> {
        private final AnnotatedType<T> type;
        private final Set<AnnotatedMethod<? super T>> methods;

        private NonBindingType(final AnnotatedType<T> annotatedType) {
            this.type = annotatedType;
            this.methods = annotatedType.getMethods().stream()
                    .map(NonBindingMethod::new)
                    .collect(toSet());
        }

        @Override
        public Class<T> getJavaClass() {
            return type.getJavaClass();
        }

        @Override
        public Set<AnnotatedConstructor<T>> getConstructors() {
            return type.getConstructors();
        }

        @Override
        public Set<AnnotatedMethod<? super T>> getMethods() {
            return methods;
        }

        @Override
        public Set<AnnotatedField<? super T>> getFields() {
            return type.getFields();
        }

        @Override
        public <T1 extends Annotation> Set<T1> getAnnotations(final Class<T1> annotationType) {
            return type.getAnnotations(annotationType);
        }

        @Override
        public Type getBaseType() {
            return type.getBaseType();
        }

        @Override
        public Set<Type> getTypeClosure() {
            return type.getTypeClosure();
        }

        @Override
        public <X extends Annotation> X getAnnotation(final Class<X> aClass) {
            return type.getAnnotation(aClass);
        }

        @Override
        public Set<Annotation> getAnnotations() {
            return type.getAnnotations();
        }

        @Override
        public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
            return type.isAnnotationPresent(aClass);
        }
    }

    private static class NonBindingMethod<A> implements AnnotatedMethod<A> {
        private final AnnotatedMethod<A> delegate;
        private final Set<Annotation> annotations;

        private NonBindingMethod(final AnnotatedMethod<A> delegate) {
            this.delegate = delegate;
            this.annotations = Stream.concat(
                            delegate.getAnnotations().stream(),
                            Stream.of(Nonbinding.Literal.INSTANCE))
                    .collect(toSet());
        }

        @Override
        public Method getJavaMember() {
            return delegate.getJavaMember();
        }

        @Override
        public <T extends Annotation> Set<T> getAnnotations(final Class<T> annotationType) {
            return delegate.getAnnotations(annotationType);
        }

        @Override
        public List<AnnotatedParameter<A>> getParameters() {
            return delegate.getParameters();
        }

        @Override
        public boolean isStatic() {
            return delegate.isStatic();
        }

        @Override
        public AnnotatedType<A> getDeclaringType() {
            return delegate.getDeclaringType();
        }

        @Override
        public Type getBaseType() {
            return delegate.getBaseType();
        }

        @Override
        public Set<Type> getTypeClosure() {
            return delegate.getTypeClosure();
        }

        @Override
        public <T extends Annotation> T getAnnotation(final Class<T> aClass) {
            return aClass == Nonbinding.class ? aClass.cast(Nonbinding.Literal.INSTANCE) : delegate.getAnnotation(aClass);
        }

        @Override
        public Set<Annotation> getAnnotations() {
            return annotations;
        }

        @Override
        public boolean isAnnotationPresent(final Class<? extends Annotation> aClass) {
            return Nonbinding.class == aClass || delegate.isAnnotationPresent(aClass);
        }
    }
}

To support method only level commands you can also add:

    public <T> void markAtClassLevelMethodOnlyCommands(@Observes final ProcessAnnotatedType<T> pat) {
        final var annotatedType = pat.getAnnotatedType();
        if (!annotatedType.isAnnotationPresent(Command.class) &&
                annotatedType.getMethods().stream().anyMatch(m -> m.isAnnotationPresent(Command.class))) {
            pat.configureAnnotatedType().add(new AnnotationLiteral<Command>() { // todo: extract CommandQualifier from main to make it a constant if this impl is desired 
            });
        }
    }

I tend to avoid to use this since it makes the extenson o(n) instead of o(1).
I also use the extension without the SPI registration using CDI SE API:

public static void main(final String... args) throws Exception {
        try (final var container = SeContainerInitializer.newInstance()
                .addExtensions(new CrestCommandQualifierExtension())
                .initialize()) {
            new CdiCrestMain(container.getBeanManager(), new SystemPropertiesDefaultsContext()).apply(args);
        }
}

provide a crest bus

Crest allows to define pretty elegant-ly commands then it would be pretty sexy to use it for CQRS. only blocking ATM: no java friendly bus API but only a command line friendly API.

Idea would be to allow:

X result = bus.sendCommand(MyCommand.class, X.class, arg1, arg2, arg3, ...);

Once this is possible we can go further and allow to create proxies for this:

public interface MyCommands {
    @ExecutionConfigiruton(MyCommand.class)
    X executeMyCommand(Arg1 arg1, Arg2 arg2, Arg3 arg3);
}

Then first snippet becomes:

X result = commands.executeMyCommand(arg1, arg2, arg3, ...);

with

commands = CrestBusFactory.create(MyCommands.class);

Add an enriched type (+editor) for files

When taking an output or input as parameter you need the stream. It is common to keep the name and to be able to switch from stdout/stderr to a file.

I use it (just output in my case but could be renamed Stream):

public class Output {
    private final PrintStream stream;
    private final String name;

    public Output(final PrintStream stream, final String name) {
        this.stream = stream;
        this.name = name;
    }

    public PrintStream getStream() {
        return stream;
    }

    public String getName() {
        return name;
    }
}

With the editor:

@Editor(Output.class)
public class OutputEditor extends AbstractConverter {
    @Override
    protected Object toObjectImpl(final String text) {
        switch (text) {
            case "stdout": return new Output(new NoClosePrintStream(System.out), text);
            case "stderr": return new Output(new NoClosePrintStream(System.err), text);
            default:
                try {
                    return new Output(new PrintStream(new FileOutputStream(text)), text);
                } catch (final FileNotFoundException e) {
                    throw new IllegalArgumentException(e);
                }
        }
    }

    private static class NoClosePrintStream extends PrintStream {
        public NoClosePrintStream(final PrintStream out) {
            super(out);
        }

        @Override
        public void close() {
            flush();
        }
    }
}

Side note: see EditorLoader issue to know about @Editor

Crest Plugin that Scans via XBean at build time

crest-xstatic-plugin

If commands are turned into an uber-jar, the xbean scanning becomes very very slow.

We could scan for the classes containing @command then put them in a file in the target/classes/ directory, then write a Commands.Loader implementation that can grab that file.

Injected streams do not play work together with argument arrays

When defining a command that takes streams and an argument array the argument array is not properly recognized.

Example: Define a command like this:

        @org.tomitribe.crest.api.Command
        public static String withArgsArray(
                                       @Err PrintStream err,
                                       @Out final PrintStream out,
                                       String[] args

        ) {...}

When invoking this command with the arguments orange and juice I get this error:

Excess arguments: orange, juice

Usage: withArgsArray [options] String...


java.lang.IllegalArgumentException: Excess arguments: orange, juice
    at org.tomitribe.crest.cmds.CmdMethod.convert(CmdMethod.java:518)
...

support parameter injection

Would be nice to be able to delegate some parameter injections (ie the unknown ones without @options) to a provider. Would allow service injection for instance

Eliminate IllegalArgumentException "No interceptor for " issue with @CrestInterceptor

Previously a declaration such as this would fail unless the RedInterceptor class was explicitly added to the list of command classes elsewhere.

    @Command(interceptedBy = RedInterceptor.class)
    public String red() {
        return "red";
    }

This change removes that issue. The interceptor will be constructed as needed with no need to declare it anywhere other than in @Command(interceptedBy

Builder API for creating the CLI

Builders can be used to construct the exact environment needed.

For example:

import org.tomitribe.crest.Main;

public class Example {

    public static void main(String[] args) {
        final Main main = Main.builder()
                .properties(System.getProperties())
                .env(System.getenv())
                .out(System.out)
                .err(System.err)
                .in(System.in)
                .exit(System::exit)
                .command(PizzaCommands.class)
                .command(SandwichCommands.class)
                .build();

        main.run(args);
    }
}

The above can be reduced further using the following convenience builder method that defaults most the above:

import org.tomitribe.crest.Main;

public class Example {

    public static void main(String[] args) {
        final Main main = Main.systemDefaults()
                .command(PizzaCommands.class)
                .command(SandwichCommands.class)
                .build();

        main.run(args);
    }
}

EditorLoader in crest main

With crest you often need custom editors. When using xbean would be gret to get them loaded automatically. I used this code to do so (side note: I didnt check xbean was really optional so something to do before including it blindly - same loader system as for scanning could be used btw):

@Retention(RUNTIME)
@Target(TYPE)
public @interface Editor {
    Class<?> value();
}
import java.beans.PropertyEditorManager;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ServiceLoader;

import org.apache.xbean.finder.Annotated;
import org.apache.xbean.finder.AnnotationFinder;
import org.apache.xbean.finder.UrlSet;
import org.apache.xbean.finder.archive.Archive;
import org.apache.xbean.finder.archive.ClasspathArchive;
import org.apache.xbean.finder.archive.CompositeArchive;
import org.tomitribe.util.JarLocation;

public class EditorLoader {
    static {
        try {
            new XBeanLoader().load();
        } catch (final Throwable skip) {
            // no-op
        }
        for (final Editor editor : ServiceLoader.load(Editor.class)) // class MyEditor extends AbstractConverter implements Editor
        {
            try {
                PropertyEditorManager.registerEditor(editor.value(), editor.getClass());
            } catch (final Exception e) {
                // no-op
            }
        }
    }

    public static void load() {
        // no-op
    }

    private static Archive thisArchive() {
        try {
            final Class<?> reference = EditorLoader.class;

            final File file = JarLocation.jarLocation(reference);
            return ClasspathArchive.archive(reference.getClassLoader(), file.toURI().toURL());
        } catch (final MalformedURLException e) {
            throw new IllegalStateException(e);
        }
    }

    private static Archive cpArchive() {
        try {
            final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            return new ClasspathArchive(
                    classLoader,
                    new UrlSet(classLoader).excludeJvm().exclude(classLoader.getParent()).getUrls());
        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }
    }

    // just allow lazy loading and avoid issues if XBean is not here
    private static class XBeanLoader {
        public void load() {
            final AnnotationFinder finder = new AnnotationFinder(new CompositeArchive(thisArchive(), cpArchive())).enableFindSubclasses();
            for (final Annotated<Class<?>> clazz : finder.findMetaAnnotatedClasses(Editor.class)) {
                PropertyEditorManager.registerEditor(clazz.getAnnotation(Editor.class).value(), clazz.get());
            }
        }
    }
}

and I wrapped crest main:

// just a wrapper to get custom editors
public class Main {
    public static void main(final String[] args) throws Exception {
        EditorLoader.load();
        org.tomitribe.crest.Main.main(args);
    }

    private Main() {
        // no-op
    }
}

Then to write a custom editor you just need to either decorate it with @Editor(editorClassKey) or without XBean also implement Editor (to use ServiceLoader - note this part could be removed and if we have a EditorLoader just use it to list editor classes with @Editor annotation which would be easier and more consistent).

simple example without uber jar

Hi,
Could you provide a very simple example without uber jar, like your AnyClass

package test;

import org.tomitribe.crest.api.Command;
import org.tomitribe.crest.api.Default;
import org.tomitribe.crest.api.Option;

public class AClass {
    @Command
    public void doit(@Option("name") String name,
            @Option("fname") @Default("foo") String fname, int age) {

        System.out.println("-> name = " + name + ", fname = " + fname
            + ", age = " + age);
    }   
}

How to run this example in a console, tomitribe-crest-1.0.0-SNAPSHOT.jar being in classpath?
$ java -jar .\cliWithCrest.jar doit
aucun attribut manifest principal dans .\cliWithCrest.jar
(~ no main manifest attribute in cliWithCrest.jar in english environment).

Shouldn't java test.Aclass doit work or have I missed something?

Thanks for any help,
jgr

PrintingOutput as alternative to StreamingOutput

Getting quite tiresome to always do:

    @Command
    public StreamingOutput list() {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream os) throws IOException {
                final PrintStream printStream = new PrintStream(os);
                // use the printStream 
            }
        };
    }

For backwards compatibility we should add a new interface PrintingOutput

package org.tomitribe.crest.api;

import java.io.IOException;
import java.io.PrintStream;

public interface PrintingOutput {
    void write(PrintStream out) throws IOException;
}

@Err and @Out seem to mess up args list

This does not appear to work:

@Command
public void awaitCapacity(
        @Option("retry") @Default("5 seconds") final Duration retry,
        @Option("timeout") @Default("1 hour") final Duration timeout,
        @Err PrintStream err,
        @Out PrintStream out,
        final OperationId operationId, final int capacity) throws TimeoutException {

When you go to use the command you get:

$ ./target/blackops operation awaitCapacity op-hui4hjozywbb4 1
Missing argument: OperationId

Usage: operation awaitCapacity [options] OperationId int

Options: 
  --retry=<Duration>       default: 5 seconds
  --timeout=<Duration>     default: 1 hour

support parameter interceptors

When integrating crest in a custom CLI you quickly need parameter processing (add filtering, swap some of them etc)

would be great to make it trivial to add (in Main?)

move helpprocessor in crest-processor

Goal is to ensure build only code is scoped to build - in particular cause annot proc can have side effects in builds.

Side note: can be neat to set supported java version to avoid the warning about it since default is java 6 so never match a compatible creste release. LATEST or any wildcard value would be welcomed.

Support 'Stream<String>' return types for commands

For example:

    @Command
    public Stream<String> listImports(final Dir dir) {
        return dir.searchJava()
                .map(Java::parse)
                .map(Java::getImports)
                .flatMap(Collection::stream);
    }

The contract would be each line is printed as a single line. The newline would be added automatically and need not be present in the individual string values.

support inline documentation

Using a properties to document an option is not always as fluent and tempting than an annotation, maybe we should introduce @Help

Mutually exclusive options & help

Something we might possibly support. Not sure what kind of an API we'd make for this, but here are some use cases. Couldn't find many commands that acknowledge this formally

tail

 tail [-F | -f | -r] [-q] [-b number | -c number | -n number] [file ...]

F, f & r are mutually exclusive. b,c,n are mutually exclusive.

tar

tar {-t | -x} [options] [patterns]

t and x are mutually exclusive

jar

The synopsis is a bit large but c, u, x, t, i are all mutually exclusive

   jar c[v0M]f jarfile [ -C dir ] inputfiles [ -Joption ]
   jar c[v0]mf manifest jarfile [ -C dir ] inputfiles [ -Joption ]
   jar c[v0M] [ -C dir ] inputfiles [ -Joption ]
   jar c[v0]m manifest [ -C dir ] inputfiles [ -Joption ]

   Update jar file
   jar u[v0M]f jarfile [ -C dir ] inputfiles [ -Joption ]
   jar u[v0]mf manifest jarfile [ -C dir ] inputfiles [ -Joption ]
   jar u[v0M] [ -C dir ] inputfiles [ -Joption ]
   jar u[v0]m manifest [ -C dir ] inputfiles [ -Joption ]

   Extract jar file
   jar x[v]f jarfile [ inputfiles ] [ -Joption ]
   jar x[v] [ inputfiles ] [ -Joption ]

   List table of contents of jar file
   jar t[v]f jarfile [ inputfiles ] [ -Joption ]
   jar t[v] [ inputfiles ] [ -Joption ]

   Add index to jar file
   jar i jarfile [ -Joption ]

cut

b, c, f are mutually exclusive

 cut -b list [-n] [file ...]
 cut -c list [file ...]
 cut -f list [-d delim] [-s] [file ...]

Parameter values are converted to lowercase when passed on the command line

Command line prameter is "--targetLocale=zh_TW" (Mixed case value)

@option("targetLocale") final Locale targetLocale //Preferred method
@option("targetLocale") final String targetLocale //Tested, but same result

Either option produces a lowercase value "zh_tw" - So the command line parameter case is NOT honoured.

Locale z = new Locale("zh_zw"); is not validated, but is also NOT a valid locale.
org.apache.commons.lang3.LocaleUtils.toLocale(z.toString()); is validated and throws an error.

The command and value case must not be converted to lowercase.

Null returned when using @Option and Bean Validation

If you create a command with next option:

@Option("myfile") @Exists @Readable File myfile

and when you do the java -jar .... you don't pass myfile option, a null is returned when in theory it should work since option is marked as optionable.

Tested in Crest 0.8.

Support nested tables

Right now to support nested table we must either reimplement it or reuse TableInterceptor with some plumbing:

@Command
@ApplicationScoped
public class Commands {
    @Inject
    private JiraService jira;

    @Table(border = asciiSeparated)
    @Command(value = "list-projects")
    public List<FormattedProject> listProjects(@Option("jira-login") final JiraService.JiraLogin login,
                                                                       @Option("jira-client") final JiraService.JiraClient configuration) {
        final var formatter = new TableInterceptor();
        return jira.findProjectsWithTasks(login, configuration).stream()
                .map(p -> new FormattedProject(p.id(), p.label(), asTable(formatter, p.tasks())))
                .collect(toList());
    }

    @Table(border = asciiSeparated)
    private String asTable(final TableInterceptor formatter, final List<JiraService.Task> tasks) {
        final var formatted = formatter.intercept(new CrestContext() {
            @Override
            public Object proceed() {
                return tasks;
            }

            @Override
            public Method getMethod() {
                try {
                    return Commands.class.getDeclaredMethod("asTable", TableInterceptor.class, List.class);
                } catch (final NoSuchMethodException e) {
                    throw new IllegalStateException(e);
                }
            }

            @Override
            public List<Object> getParameters() {
                return List.of();
            }

            @Override
            public String getName() {
                return "formatAsTable";
            }

            @Override
            public List<ParameterMetadata> getParameterMetadata() {
                return List.of();
            }
        });
        if (formatted instanceof PrintOutput po) {
            final var out = new ByteArrayOutputStream();
            try (final var ps = new PrintStream(out)) {
                po.write(ps);
            } catch (final IOException e) {
                throw new IllegalStateException(e);
            }
            return out.toString(UTF_8);
        }
        throw new IllegalStateException("Unknown type: " + formatted);
    }

    public record FormattedProject(String id, String label, String tasks) {
    }
}

This works but I would expect asTable to be replacable by @Table on the nested type (public record Project(String id, String label, @Table(...config...) List<Task> tasks) { }) which would make it simpler but would also avoid to depend on the implementation module in commands.

Tip if you need the same feature: you can move this code in an utility class and call it to preformat your data before the rendering:

@Table(border = asciiSeparated)
@Command(value = "list-projects")
public List<FormattedProject> listProjects(....) {
    return jira.findProjects(login, configuration).stream()
            .map(p -> new FormattedProject(p.id(), p.label(), AsTable.asTable(p.tasks()))) // here is the trick, AsTable is previous code in a static utility class
            .collect(toList());
}

Add a Mojo (mvn plugin) to generate all UNIx command scripts from found commands

If my module has these commands:

  • foo --options=cfrf
  • bar dummy --op=s
  • ...

then I should get the following scripts:

  • foo // usage: ./foo --options=cfrf
  • bar-dummy // usage: ./bar-dummy --op=s

The scripts will support help:

./foo help

will execute the command help foo

and

./bar-dummy help

would execute the command help bar dummy

To find commands the Mojo will support:

Finally includes/excludes option and outputFolder will be added as configurable parameter to the Mojo.

Side note: it is probably a good idea to add a __find_java script (the name should just not conflict with commands) to share the logic of the JAVA finding then to get java command scripts can do:

proc_script_base=`cd $(dirname $0) && cd .. && pwd`
source "$(dirname $0)/__find_java"
$JAVA ....

Tip: https://github.com/apache/tomee/blob/master/tomee/apache-tomee/src/main/resources/tomee.sh#L24 or tomcat scripts can be used as model to find the right java command

For the classpath the same trick will be applied using another script not generated for the first version (another issue can be opened to generate a full assembly). It means the user will have to provider a __set_classpath script initializing CREST_CLASSPATH environment variable and scripts will just reuse this.

Excluding fields from @Table output

When creating a table, sometimes you want all the fields minus one or two. It would be great if specific fields could be excluded from the output. Perhaps something like @Table(fields = "!createdAt")

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.